惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
B
Blog
Last Week in AI
Last Week in AI
Jina AI
Jina AI
WordPress大学
WordPress大学
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
P
Proofpoint News Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
Google DeepMind News
Google DeepMind News
U
Unit 42
雷峰网
雷峰网
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
MongoDB | Blog
MongoDB | Blog
F
Fortinet All Blogs
美团技术团队
Y
Y Combinator Blog
腾讯CDC
B
Blog RSS Feed
T
The Blog of Author Tim Ferriss
Microsoft Azure Blog
Microsoft Azure Blog
Recent Announcements
Recent Announcements

博客园 - 邗影

TOF传感器 opengl渲染YUV的波浪百叶窗效果 传感器 关于音频PA结构腔体 球面渲染 关于linux和shi脚本常用的通配符 关于fread读不满一块和读不满count的返回值 如何去掉merge UBI的覆盖写和擦除 DTSI 多个模块共用同一个 GPIO 引脚冲突 问题汇总 debug比release程序启动的快 关于CPU占用优先级的调整 关于蜂鸣器发声 麦序与声源定位 关于squashfs压缩挂载 软连接生成 线程退出未定义行为 linux文件查找 C语言弱函数 数据传输报错port unreachable D3D11绘制三角形 关于延迟测试 v4l2检测 关于数据库的性能 流媒体小记包含一些面试问题 v4l2视频采集 流媒体ZLM配置vscode远程开发 关于SPS中的帧率问题 Linux编译问题 SSl冲突问题
3D融合场景demo
邗影 · 2026-08-20 · via 博客园 - 邗影

1:搭建glm+glfw+assimp环境;可以通过Nuget直接安装;(参考:vs2022配置Assimp环境 - 捞的不谈 - 博客园)

2:导入3D模型,模型来时learn opengl ,期间你需要引入一些直接开发的头文件,比如share.cpp,camera,mesh;要自己写shader 进行蒙皮着色,这里不引入骨骼

3: 3D 模型加载进来,开发根据鼠标移动,或者鼠标控制左右转,或者中MVP绕坐标旋转,这里要注意视角,模式等;

4:有个3D模型我们,可能常用的是在实时采集视频里引入3D,这个时候就涉及多层渲染,引入普通YUV,放在底层,不设置深度缓冲;这个YUV就充当了实时视频的角色;可以加些马赛克,掩藏背景;由于我这个YUV背景是外部环境,所以加了马赛克;

5:渲染时候显示中英文,安装freetype即可添加文字渲染shader;freetype由于Windows上直接安装的版本与vs可能不兼容,需要手动在项目属性里引入下静态lib;

6:给任务加些其他特效,比如脚底冒烟,闪电;冒烟采用随机粒子渲染,闪电采用随机分叉;分别渲染两个不同的层;因为他们其实没有什么相关性;

7:我们常用离屏渲染,以方便既可以上屏,又可以readpixel存图,所以增加了FBO+readpixel逻辑;

8:希望按下按键3D 小人抖动一下,抖动节奏按照比塞尔曲线衰减,抖动就是移动,移动的幅度跟贝塞尔斜率相关;所以通过shader+offset+贝塞尔抖动,让他能在几秒内完成抖动,配合脚底的例子光球一起;

9:鉴于背景用的现实场景,可以加些马赛克或者背景虚化,由于我的YUV和3D小人不是渲染在同一层的,所以背景可以完全虚化;并不需要人像抠图+区域虚化;

 glfwSwapInterval(1);可以控制按照垂直同步刷新帧率fps60

由于没有录屏,截图瞬时无法截图到震动和闪电效果;看几个随机截图的静态状态吧:(原创禁止转载

output_002

output_000

output_001

下边是完整代码:

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#include "shader.h"
#include "model.h"
#include "freetype_text.h"

#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cstdlib>

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

const int YUV_W = 800;
const int YUV_H = 600;
const char* YUV_FILE = "camera_800x600_306632.yuv";

const int FBO_W = 1280;
const int FBO_H = 720;

float lastFrame = 0.0f;
int fps = 0;
float fpsTimer = 0.0f;
int frameCount = 0;

bool needSaveFrame = false;

bool isShaking = false;
float shakeTimer = 0.0f;
const float SHAKE_DURATION = 2.5f;
const float SHAKE_AMPLITUDE = 0.5f;

float bezierShakeEnvelope(float t)
{
    float u = 1.0f - t;
    return u * u * 1.0f + 2.0f * u * t * 0.3f + t * t * 0.0f;
}

GLuint fbo = 0;
GLuint fboColorTex = 0;
GLuint fboDepthRbo = 0;

// [新增] 粒子系统
const int MAX_PARTICLES = 300;
struct Particle {
    glm::vec3 pos;
    glm::vec3 vel;
    float life;
    float maxLife;
};
std::vector<Particle> particles;
GLuint particleVAO = 0;
GLuint particleVBO = 0;

// [新增] 分形闪电
struct LightningBolt {
    std::vector<glm::vec3> vertices;
};
LightningBolt bolt;
float boltLife = 0.0f;
const float BOLT_DURATION = 0.12f;
GLuint lightningVAO = 0;
GLuint lightningVBO = 0;

float quadVertices[] = {
    -1.0f,  1.0f,  0.0f, 1.0f,
    -1.0f, -1.0f,  0.0f, 0.0f,
     1.0f, -1.0f,  1.0f, 0.0f,
    -1.0f,  1.0f,  0.0f, 1.0f,
     1.0f, -1.0f,  1.0f, 0.0f,
     1.0f,  1.0f,  1.0f, 1.0f
};

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

void initFBO()
{
    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);

    glGenTextures(1, &fboColorTex);
    glBindTexture(GL_TEXTURE_2D, fboColorTex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, FBO_W, FBO_H, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboColorTex, 0);

    glGenRenderbuffers(1, &fboDepthRbo);
    glBindRenderbuffer(GL_RENDERBUFFER, fboDepthRbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, FBO_W, FBO_H);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fboDepthRbo);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "ERROR: FBO incomplete!" << std::endl;

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void RGBAToNV12(unsigned char* rgba, int w, int h, unsigned char* yuv)
{
    unsigned char* yPlane = yuv;
    unsigned char* uvPlane = yuv + w * h;

    std::vector<int> yBuf(w * h);
    std::vector<int> uBuf(w * h);
    std::vector<int> vBuf(w * h);

    for (int y = 0; y < h; y++)
    {
        for (int x = 0; x < w; x++)
        {
            int srcIdx = ((h - 1 - y) * w + x) * 4;
            int r = rgba[srcIdx];
            int g = rgba[srcIdx + 1];
            int b = rgba[srcIdx + 2];

            int dstIdx = y * w + x;
            yBuf[dstIdx] = (int)(0.299f * r + 0.587f * g + 0.114f * b);
            uBuf[dstIdx] = (int)(-0.169f * r - 0.331f * g + 0.499f * b + 128.0f);
            vBuf[dstIdx] = (int)(0.499f * r - 0.419f * g - 0.081f * b + 128.0f);
        }
    }

    for (int i = 0; i < w * h; i++)
    {
        int yv = yBuf[i];
        yPlane[i] = (unsigned char)(yv < 0 ? 0 : (yv > 255 ? 255 : yv));
    }

    int uvW = w / 2;
    int uvH = h / 2;
    for (int y = 0; y < uvH; y++)
    {
        for (int x = 0; x < uvW; x++)
        {
            int y00 = (y * 2) * w + (x * 2);
            int y01 = (y * 2) * w + (x * 2 + 1);
            int y10 = (y * 2 + 1) * w + (x * 2);
            int y11 = (y * 2 + 1) * w + (x * 2 + 1);

            int uAvg = (uBuf[y00] + uBuf[y01] + uBuf[y10] + uBuf[y11]) / 4;
            int vAvg = (vBuf[y00] + vBuf[y01] + vBuf[y10] + vBuf[y11]) / 4;

            uAvg = uAvg < 0 ? 0 : (uAvg > 255 ? 255 : uAvg);
            vAvg = vAvg < 0 ? 0 : (vAvg > 255 ? 255 : vAvg);

            int uvIdx = (y * uvW + x) * 2;
            uvPlane[uvIdx] = (unsigned char)uAvg;
            uvPlane[uvIdx + 1] = (unsigned char)vAvg;
        }
    }
}

// [新增] 初始化粒子 VAO/VBO
void initParticles()
{
    glGenVertexArrays(1, &particleVAO);
    glGenBuffers(1, &particleVBO);
    glBindVertexArray(particleVAO);
    glBindBuffer(GL_ARRAY_BUFFER, particleVBO);
    glBufferData(GL_ARRAY_BUFFER, MAX_PARTICLES * 5 * sizeof(float), NULL, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
    glBindVertexArray(0);
}

// [新增] 更新粒子
void updateParticles(float dt, glm::vec3 emitPos)
{
    static float emitTimer = 0.0f;
    emitTimer += dt;

    // [修改] 降低发射频率,光球更大更持久
    while (emitTimer > 0.06f && (int)particles.size() < MAX_PARTICLES)
    {
        emitTimer -= 0.06f;
        Particle p;

        // 脚底圆形区域发射
        float angle = (rand() % 360) * 3.14159f / 180.0f;
        float radius = (rand() % 100) / 400.0f;
        p.pos = emitPos + glm::vec3(cos(angle) * radius, 0.0f, sin(angle) * radius);

        // [修改] 向上飘散,带初始螺旋速度
        float upSpeed = 0.4f + (rand() % 100) / 300.0f;
        p.vel = glm::vec3(cos(angle) * 0.5f, upSpeed, sin(angle) * 0.5f);

        p.life = 1.0f;
        p.maxLife = 2.0f + (rand() % 100) / 100.0f;
        particles.push_back(p);
    }

    for (auto& p : particles)
    {
        p.pos += p.vel * dt;

        // [新增] 螺旋上升:水平速度每帧旋转
        float rotSpeed = 3.0f * dt;
        float cx = cos(rotSpeed);
        float sz = sin(rotSpeed);
        float vx = p.vel.x * cx - p.vel.z * sz;
        float vz = p.vel.x * sz + p.vel.z * cx;
        p.vel.x = vx;
        p.vel.z = vz;

        // [新增] 越往上升越慢(灵气阻力感)
        p.vel *= (1.0f - dt * 0.2f);

        p.life -= dt * 0.3f;
    }

    particles.erase(std::remove_if(particles.begin(), particles.end(),
        [](const Particle& p) { return p.life <= 0.0f; }), particles.end());

    std::vector<float> data;
    data.reserve(particles.size() * 5);
    for (auto& p : particles)
    {
        data.push_back(p.pos.x);
        data.push_back(p.pos.y);
        data.push_back(p.pos.z);
        float lifeRatio = glm::clamp(p.life / p.maxLife, 0.0f, 1.0f);
        data.push_back(lifeRatio);

        // [修改] 光球大小:出生小 -> 中间膨胀大 -> 消失小
        float size = 0.3f + sin(lifeRatio * 3.14159f) * 0.7f;
        data.push_back(size);
    }

    if (!data.empty())
    {
        glBindBuffer(GL_ARRAY_BUFFER, particleVBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, data.size() * sizeof(float), data.data());
    }
}

// [新增] 渲染粒子
void renderParticles(Shader& shader, const glm::mat4& view, const glm::mat4& projection, int count)
{
    if (count <= 0) return;
    glEnable(GL_PROGRAM_POINT_SIZE);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE); // 加法混合,越叠越亮
    glDepthMask(GL_FALSE); // 粒子不写入深度,避免遮挡问题

    shader.use();
    shader.setMat4("view", view);
    shader.setMat4("projection", projection);
    glBindVertexArray(particleVAO);
    glDrawArrays(GL_POINTS, 0, count);

    glDepthMask(GL_TRUE);
    glDisable(GL_BLEND);
    glDisable(GL_PROGRAM_POINT_SIZE);
}
void initLightning()
{
    glGenVertexArrays(1, &lightningVAO);
    glGenBuffers(1, &lightningVBO);
    glBindVertexArray(lightningVAO);
    glBindBuffer(GL_ARRAY_BUFFER, lightningVBO);
    glBufferData(GL_ARRAY_BUFFER, 10000 * sizeof(glm::vec3), NULL, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glBindVertexArray(0);
}

void buildLightningRecursive(glm::vec3 a, glm::vec3 b, int depth, float offsetScale)
{
    if (depth <= 0) {
        bolt.vertices.push_back(a);
        bolt.vertices.push_back(b);
        return;
    }

    glm::vec3 mid = (a + b) * 0.5f;
    glm::vec3 dir = b - a;

    glm::vec3 randVec(
        (rand() % 1000 - 500) / 500.0f,
        (rand() % 1000 - 500) / 500.0f,
        (rand() % 1000 - 500) / 500.0f
    );
    glm::vec3 perp = glm::normalize(glm::cross(dir, randVec));
    if (glm::length(perp) < 0.01f) perp = glm::vec3(1, 0, 0);

    float offset = ((rand() % 1000) / 1000.0f - 0.5f) * 2.0f * offsetScale;
    mid += perp * offset;

    if (depth > 2 && (rand() % 100) < 45) {
        glm::vec3 branchDir = glm::normalize(glm::vec3(
            (rand() % 1000 - 500) / 500.0f,
            (rand() % 1000 - 500) / 1000.0f,
            (rand() % 1000 - 500) / 500.0f
        ));
        float branchLen = glm::length(dir) * (0.3f + (rand() % 500) / 1000.0f);
        glm::vec3 branchEnd = mid + branchDir * branchLen;
        buildLightningRecursive(mid, branchEnd, depth - 2, offsetScale * 0.5f);
    }

    buildLightningRecursive(a, mid, depth - 1, offsetScale * 0.5f);
    buildLightningRecursive(mid, b, depth - 1, offsetScale * 0.5f);
}

void spawnLightning(glm::vec3 start, glm::vec3 end)
{
    bolt.vertices.clear();
    buildLightningRecursive(start, end, 6, 1.2f);
    boltLife = BOLT_DURATION;

    if (!bolt.vertices.empty()) {
        glBindBuffer(GL_ARRAY_BUFFER, lightningVBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0,
            bolt.vertices.size() * sizeof(glm::vec3),
            bolt.vertices.data());
    }
}

void renderLightning(Shader& shader, const glm::mat4& view, const glm::mat4& projection, float dt)
{
    if (boltLife <= 0.0f) return;
    boltLife -= dt;

    float intensity = 1.0f;
    if (boltLife < BOLT_DURATION * 0.5f) {
        intensity = boltLife / (BOLT_DURATION * 0.5f);
    }

    glDisable(GL_DEPTH_TEST);
    glLineWidth(3.0f);

    shader.use();
    shader.setMat4("view", view);
    shader.setMat4("projection", projection);
    shader.setVec3("boltColor", glm::vec3(0.9f, 0.95f, 1.0f));
    shader.setFloat("intensity", intensity);

    glBindVertexArray(lightningVAO);
    glDrawArrays(GL_LINES, 0, (GLsizei)bolt.vertices.size());

    glEnable(GL_DEPTH_TEST);
}

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Model + Smoke + Shake", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    srand((unsigned int)glfwGetTime());

    initFBO();
    initParticles();
    initLightning(); // [新增]闪电

    size_t ySize = YUV_W * YUV_H;
    size_t uvSize = YUV_W * YUV_H / 2;
    size_t totalSize = ySize + uvSize;
    std::vector<unsigned char> yuvData(totalSize);

    std::ifstream file(YUV_FILE, std::ios::binary);
    if (!file)
    {
        std::cout << "ERROR: Failed to open YUV file: " << YUV_FILE << std::endl;
        return -1;
    }
    file.read((char*)yuvData.data(), totalSize);
    file.close();

    GLuint yTex, uvTex;
    glGenTextures(1, &yTex);
    glBindTexture(GL_TEXTURE_2D, yTex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, YUV_W, YUV_H, 0, GL_RED, GL_UNSIGNED_BYTE, yuvData.data());
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glGenTextures(1, &uvTex);
    glBindTexture(GL_TEXTURE_2D, uvTex);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RG, YUV_W / 2, YUV_H / 2, 0, GL_RG, GL_UNSIGNED_BYTE, yuvData.data() + ySize);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    GLuint quadVAO, quadVBO;
    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);
    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
    glBindVertexArray(0);

    Shader yuvShader("yuv_background.vert", "yuv_background.frag");
    Shader modelShader("model_loading.vert", "model_loading.frag");
    Shader particleShader("particle.vert", "particle.frag"); // [新增]
    Shader lightningShader("lightning.vert", "lightning.frag"); // [新增]

    FreeTypeText textRenderer;
    if (!textRenderer.init("C:/Windows/Fonts/msyh.ttc", FBO_W, FBO_H, "text.vert", "text.frag"))
    {
        std::cout << "Failed to init FreeType text renderer" << std::endl;
        return -1;
    }

    Model ourModel("nanosuit/nanosuit.obj");

    std::vector<unsigned char> rgbaPixels(FBO_W * FBO_H * 4);
    std::vector<unsigned char> yuvOutput(FBO_W * FBO_H * 3 / 2);

    lastFrame = static_cast<float>(glfwGetTime());

    while (!glfwWindowShouldClose(window))
    {
        float currentFrame = static_cast<float>(glfwGetTime());
        float deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;
        if (deltaTime > 0.1f) deltaTime = 0.016f;

        frameCount++;
        fpsTimer += deltaTime;
        if (fpsTimer >= 1.0f)
        {
            fps = frameCount;
            frameCount = 0;
            fpsTimer = 0.0f;
        }

        processInput(window);

        // ============================================
        // 阶段1:离屏渲染到 FBO
        // ============================================
        glBindFramebuffer(GL_FRAMEBUFFER, fbo);
        glViewport(0, 0, FBO_W, FBO_H);
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // ---- YUV 背景 ----
        glDisable(GL_DEPTH_TEST);
        yuvShader.use();
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, yTex);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, uvTex);
        yuvShader.setInt("yTexture", 0);
        yuvShader.setInt("uvTexture", 1);
        glBindVertexArray(quadVAO);
        glDrawArrays(GL_TRIANGLES, 0, 6);

        // ---- 模型 + 震动 ----
        glEnable(GL_DEPTH_TEST);
        modelShader.use();

        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)FBO_W / (float)FBO_H, 0.1f, 100.0f);
        modelShader.setMat4("projection", projection);

        glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.0f, 5.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
        modelShader.setMat4("view", view);

        glm::vec3 shakeOffset(0.0f);
        if (isShaking)
        {
            shakeTimer += deltaTime;
            if (shakeTimer >= SHAKE_DURATION)
            {
                isShaking = false;
                shakeTimer = 0.0f;
                std::cout << "Shake finished!" << std::endl;
            }
            else
            {
                float t = shakeTimer / SHAKE_DURATION;
                float envelope = bezierShakeEnvelope(t);
                float freqX = 80.0f + (1.0f - t) * 30.0f;
                float freqY = 60.0f + (1.0f - t) * 20.0f;
                shakeOffset.x = sin(shakeTimer * freqX) * SHAKE_AMPLITUDE * envelope;
                shakeOffset.y = cos(shakeTimer * freqY) * SHAKE_AMPLITUDE * envelope * 0.6f;
            }
        }

        float rotAngle = currentFrame * 30.0f;
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::translate(model, glm::vec3(0.0f, -1.0f, 0.0f) + shakeOffset);
        model = glm::rotate(model, glm::radians(rotAngle), glm::vec3(0.0f, 1.0f, 0.0f));
        model = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f));
        modelShader.setMat4("model", model);

        ourModel.Draw(modelShader);
        // [新增] ---- 分形闪电 ----
        renderLightning(lightningShader, view, projection, deltaTime);
        // [新增] ---- 粒子冒烟/光球(模型脚底下)----
        glm::vec3 emitPos = glm::vec3(0.0f, -1.0f, 0.0f) + shakeOffset;
        updateParticles(deltaTime, emitPos);
        renderParticles(particleShader, view, projection, (int)particles.size());

        // ---- 文字 ----
        glDisable(GL_DEPTH_TEST);
        std::wstringstream wss;
        wss << L"帧率: " << fps << L" | 空格震动+闪电| P保存YUV|粒子冒烟";
        textRenderer.renderText(wss.str(), 25.0f, FBO_H - 50.0f, 0.5f, glm::vec3(0.0f, 1.0f, 0.0f));
        glEnable(GL_DEPTH_TEST);

        // ============================================
        // 阶段2:FBO 复制到屏幕
        // ============================================
        glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
        glBlitFramebuffer(0, 0, FBO_W, FBO_H, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_COLOR_BUFFER_BIT, GL_LINEAR);

        // ============================================
        // 阶段3:从 FBO 保存 YUV
        // ============================================
        if (needSaveFrame)
        {
            glBindFramebuffer(GL_FRAMEBUFFER, fbo);
            glReadPixels(0, 0, FBO_W, FBO_H, GL_RGBA, GL_UNSIGNED_BYTE, rgbaPixels.data());
            RGBAToNV12(rgbaPixels.data(), FBO_W, FBO_H, yuvOutput.data());

            std::ofstream outFile("output.yuv", std::ios::binary);
            if (outFile)
            {
                outFile.write((char*)yuvOutput.data(), yuvOutput.size());
                outFile.close();
                std::cout << "Saved output.yuv (" << FBO_W << "x" << FBO_H << ")" << std::endl;
            }
            needSaveFrame = false;
        }

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteVertexArrays(1, &quadVAO);
    glDeleteBuffers(1, &quadVBO);
    glDeleteTextures(1, &yTex);
    glDeleteTextures(1, &uvTex);
    glDeleteTextures(1, &fboColorTex);
    glDeleteRenderbuffers(1, &fboDepthRbo);
    glDeleteFramebuffers(1, &fbo);
    glDeleteVertexArrays(1, &particleVAO);
    glDeleteBuffers(1, &particleVBO);
    glDeleteVertexArrays(1, &lightningVAO); // [新增]
    glDeleteBuffers(1, &lightningVBO);      // [新增]

    glfwTerminate();
    return 0;
}

void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    static bool spacePressedLast = false;
    bool spacePressed = glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS;
    if (spacePressed && !spacePressedLast && !isShaking)
    {
        isShaking = true;
        shakeTimer = 0.0f;
        
        // [新增] 同时劈一道闪电
        glm::vec3 boltStart(
            (rand() % 100 - 50) / 30.0f,
            4.0f,
            (rand() % 100 - 50) / 30.0f
        );
        glm::vec3 boltEnd(0.0f, -0.5f, 0.0f);
        spawnLightning(boltStart, boltEnd);

        std::cout << "Shake + Lightning triggered!" << std::endl;
    }
    spacePressedLast = spacePressed;

    static bool pPressedLast = false;
    bool pPressed = glfwGetKey(window, GLFW_KEY_P) == GLFW_PRESS;
    if (pPressed && !pPressedLast)
    {
        needSaveFrame = true;
        std::cout << "Save triggered!" << std::endl;
    }
    pPressedLast = pPressed;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

 文字渲染:

#ifndef FREETYPE_TEXT_H
#define FREETYPE_TEXT_H

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <ft2build.h>
#include FT_FREETYPE_H

#include <map>
#include <string>
#include <iostream>

#include "shader.h"

struct Character {
    GLuint textureID;
    glm::ivec2 size;
    glm::ivec2 bearing;
    GLuint advance;
};

class FreeTypeText
{
public:
    std::map<wchar_t, Character> characters; // [修改] key 改成 wchar_t
    FT_Library ft = nullptr;                 // [新增] 保留库实例,用于动态加载
    FT_Face face = nullptr;                  // [新增] 保留字体面
    GLuint VAO = 0, VBO = 0;
    Shader* textShader = nullptr;

    bool init(const char* fontPath, GLuint screenWidth, GLuint screenHeight, const char* vertPath, const char* fragPath)
    {
        textShader = new Shader(vertPath, fragPath);

        if (FT_Init_FreeType(&ft))
        {
            std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;
            return false;
        }

        if (FT_New_Face(ft, fontPath, 0, &face))
        {
            std::cout << "ERROR::FREETYPE: Failed to load font: " << fontPath << std::endl;
            return false;
        }

        FT_Set_Pixel_Sizes(face, 0, 48);

        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

        // [修改] 只预加载常用 ASCII,中文字按需加载
        for (unsigned char c = 0; c < 128; c++)
        {
            loadCharacter((wchar_t)c);
        }

        // 配置 VAO/VBO
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);

       
        // [修改] 恢复原版:正交投影原点在左下角(Y轴向上)
        glm::mat4 projection = glm::ortho(0.0f, (float)screenWidth, 0.0f, (float)screenHeight);
        textShader->use();
        textShader->setMat4("projection", projection);

        return true;
    }

    // [新增] 按需加载单个字符
    bool loadCharacter(wchar_t c)
    {
        if (characters.find(c) != characters.end())
            return true;

        if (FT_Load_Char(face, c, FT_LOAD_RENDER))
        {
            std::wcout << L"ERROR::FREETYPE: Failed to load Glyph: " << c << std::endl;
            return false;
        }

        GLuint texture;
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexImage2D(
            GL_TEXTURE_2D,
            0,
            GL_RED,
            face->glyph->bitmap.width,
            face->glyph->bitmap.rows,
            0,
            GL_RED,
            GL_UNSIGNED_BYTE,
            face->glyph->bitmap.buffer
        );

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        Character character = {
            texture,
            glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
            glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
            static_cast<GLuint>(face->glyph->advance.x)
        };
        characters.insert(std::pair<wchar_t, Character>(c, character));
        return true;
    }

    // [修改] 接收宽字符串
    void renderText(std::wstring text, float x, float y, float scale, glm::vec3 color)
    {
        textShader->use();
        textShader->setVec3("textColor", color);
        glActiveTexture(GL_TEXTURE0);
        glBindVertexArray(VAO);

        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        for (wchar_t c : text)
        {
            // [新增] 按需加载
            if (characters.find(c) == characters.end())
                loadCharacter(c);

            Character ch = characters[c];

            float xpos = x + ch.bearing.x * scale;
            float ypos = y - (ch.size.y - ch.bearing.y) * scale; // [恢复] 原版计算

            float w = ch.size.x * scale;
            float h = ch.size.y * scale;

            // [恢复] 原版 UV(LearnOpenGL 标准做法)
            float vertices[6][4] = {
                { xpos,     ypos + h,   0.0f, 0.0f },
                { xpos,     ypos,       0.0f, 1.0f },
                { xpos + w, ypos,       1.0f, 1.0f },

                { xpos,     ypos + h,   0.0f, 0.0f },
                { xpos + w, ypos,       1.0f, 1.0f },
                { xpos + w, ypos + h,   1.0f, 0.0f }
            };

            glBindTexture(GL_TEXTURE_2D, ch.textureID);
            glBindBuffer(GL_ARRAY_BUFFER, VBO);
            glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glDrawArrays(GL_TRIANGLES, 0, 6);

            x += (ch.advance >> 6) * scale;
        }

        glBindVertexArray(0);
        glBindTexture(GL_TEXTURE_2D, 0);
        glDisable(GL_BLEND);
    }
};

#endif
#version 330 core
in vec2 TexCoords;
out vec4 color;

uniform sampler2D text;
uniform vec3 textColor;

void main()
{
    vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
    color = vec4(textColor, 1.0) * sampled;
}



#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 pos, vec2 tex>
out vec2 TexCoords;

uniform mat4 projection;

void main()
{
    gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
    TexCoords = vertex.zw;
}

//文字的shader

  背景YUV渲染:

#version 330 core
in vec2 TexCoord;
out vec4 FragColor;

uniform sampler2D yTexture;
uniform sampler2D uvTexture;

float hash(vec2 p)
{
    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}

void main()
{
    float y = texture(yTexture, TexCoord).r;
    float u = texture(uvTexture, TexCoord).r - 0.5;
    float v = texture(uvTexture, TexCoord).g - 0.5;

    float r = y + 1.402 * v;
    float g = y - 0.344136 * u - 0.714136 * v;
    float b = y + 1.772 * u;

    vec3 baseColor = vec3(r, g, b);

    vec2 blocks = vec2(40.0, 30.0);
    vec2 cellId = floor(TexCoord * blocks);
    vec2 inside = fract(TexCoord * blocks);

    float rnd = hash(cellId);
    float rnd2 = hash(cellId + vec2(13.37, 73.31));
    float rnd3 = hash(cellId + vec2(51.17, 37.73));

    float angle = rnd * 6.28318;
    float c = cos(angle);
    float s = sin(angle);
    vec2 rot = vec2(
        c * (inside.x - 0.5) - s * (inside.y - 0.5),
        s * (inside.x - 0.5) + c * (inside.y - 0.5)
    ) + 0.5;

    vec2 sampleCoord = (cellId + rot) / blocks;
    sampleCoord += (vec2(rnd2, rnd3) - 0.5) * 0.025;

    float y2 = texture(yTexture, sampleCoord).r;
    float u2 = texture(uvTexture, sampleCoord).r - 0.5;
    float v2 = texture(uvTexture, sampleCoord).g - 0.5;

    float r2 = y2 + 1.402 * v2;
    float g2 = y2 - 0.344136 * u2 - 0.714136 * v2;
    float b2 = y2 + 1.772 * u2;

    vec3 color = vec3(r2, g2, b2);

    float lum = dot(color, vec3(0.299, 0.587, 0.114));
    color *= 0.85 + lum * 0.3;

    vec2 edgeDist = abs(inside - 0.5);
    float edge = max(edgeDist.x, edgeDist.y);
    float border = smoothstep(0.47, 0.5, edge);

    float edgeGlow = border * (0.4 + rnd * 0.6);
    vec3 glow = vec3(0.9, 0.95, 1.0) * edgeGlow * 0.5;

    vec2 hlPos = vec2(rnd2, rnd3);
    float highlight = exp(-length((inside - hlPos) * blocks) * 2.5);
    highlight *= smoothstep(0.2, 0.6, lum);
    vec3 hlColor = vec3(1.0, 0.98, 0.95) * highlight * 1.5;

    float starChance = step(0.94, rnd);
    float star = exp(-length(inside - vec2(0.5)) * blocks.x * 0.25);
    star = pow(star, 4.0) * starChance;
    vec3 starColor = vec3(1.0, 0.97, 0.9) * star * 3.0;

    color +=  hlColor + starColor;

    float vig = 1.0 - length(TexCoord - 0.5) * 0.4;
    color *= vig;

    FragColor = vec4(color, 1.0);
}
//上边这个增加了闪闪发光的马赛克的frag

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 0.0, 1.0);
TexCoord = aTexCoord;
}

 

模型就按照assimp加载的后的解析顺序渲染即可

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;

out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    TexCoords = aTexCoords;
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

#version 330 core
in vec2 TexCoords;

out vec4 FragColor;

uniform sampler2D texture_diffuse1;

void main()
{
    FragColor = texture(texture_diffuse1, TexCoords);
}

闪电和粒子光球

闪电
#version 330 core
out vec4 FragColor;

uniform vec3 boltColor;
uniform float intensity;

void main()
{
    FragColor = vec4(boltColor * intensity, 1.0);
}
#version 330 core
layout (location = 0) in vec3 aPos;

uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * vec4(aPos, 1.0);
}
粒子光球:
#version 330 core
in float vLife;
out vec4 FragColor;

void main()
{
    vec2 coord = gl_PointCoord - vec2(0.5);
    float dist = length(coord);
    if (dist > 0.5) discard;
    
    float glow = exp(-dist * dist * 8.0);
    
    vec3 coreColor = vec3(0.1, 0.0, 0.0);
    vec3 edgeColor = vec3(0.8, 0.9, 0.0);
    vec3 color = mix(coreColor, edgeColor, 1.0 - vLife);
    
    color *= 1.5 + glow * 2.0;
    
    float alpha = glow * vLife * 0.8;
    
    FragColor = vec4(color, alpha);
}
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in float aLife;
layout (location = 2) in float aSize;

uniform mat4 view;
uniform mat4 projection;

out float vLife;

void main()
{
    gl_Position = projection * view * vec4(aPos, 1.0);
    gl_PointSize = aSize * 30.0; 
    vLife = aLife;
}

辅助:相机,mesh,glad

#ifndef OPENGL_CAMERA_H
#define OPENGL_CAMERA_H

#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <vector>

// 相机移动方向枚举
enum Camera_Movement {
    FORWARD,
    BACKWARD,
    LEFT,
    RIGHT
};

// 默认相机参数
const float YAW         = -90.0f;
const float PITCH       =  0.0f;
const float SPEED       =  2.5f;
const float SENSITIVITY =  0.1f;
const float ZOOM        =  45.0f;


class Camera
{
public:
    glm::vec3 Position;
    glm::vec3 Front;
    glm::vec3 Up;
    glm::vec3 Right;
    glm::vec3 WorldUp;

    float Yaw;
    float Pitch;

    float MovementSpeed;
    float MouseSensitivity;
    float Zoom;

    // 构造函数
    Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH)
        : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
    {
        Position = position;
        WorldUp = up;
        Yaw = yaw;
        Pitch = pitch;
        updateCameraVectors();
    }

    // 获取View矩阵
    glm::mat4 GetViewMatrix()
    {
        return glm::lookAt(Position, Position + Front, Up);
    }

    // 处理键盘输入
    void ProcessKeyboard(Camera_Movement direction, float deltaTime)
    {
        float velocity = MovementSpeed * deltaTime;
        if (direction == FORWARD)
            Position += Front * velocity;
        if (direction == BACKWARD)
            Position -= Front * velocity;
        if (direction == LEFT)
            Position -= Right * velocity;
        if (direction == RIGHT)
            Position += Right * velocity;
    }

    // 处理鼠标移动
    void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true)
    {
        xoffset *= MouseSensitivity;
        yoffset *= MouseSensitivity;

        Yaw   += xoffset;
        Pitch += yoffset;

        if (constrainPitch)
        {
            if (Pitch > 89.0f)
                Pitch = 89.0f;
            if (Pitch < -89.0f)
                Pitch = -89.0f;
        }
        updateCameraVectors();
    }

    // 鼠标滚轮缩放
    void ProcessMouseScroll(float yoffset)
    {
        Zoom -= (float)yoffset;
        if (Zoom < 1.0f)
            Zoom = 1.0f;
        if (Zoom > 45.0f)
            Zoom = 45.0f;
    }

private:
    void updateCameraVectors()
    {
        glm::vec3 front;
        front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));
        front.y = sin(glm::radians(Pitch));
        front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));
        Front = glm::normalize(front);

        Right = glm::normalize(glm::cross(Front, WorldUp));
        Up    = glm::normalize(glm::cross(Right, Front));
    }
};
#endif

mesh里骨骼最终没有使用因为暂时渲染静态,没有骨骼的文件

#ifndef OPENGL_MESH_H
#define OPENGL_MESH_H

#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include "shader.h"

#include <string>
#include <vector>

// [新增] 每个顶点最多受 4 根骨骼影响
#define MAX_BONE_INFLUENCE 4

// [修改] Vertex 结构体增加骨骼绑定数据
struct Vertex {
    glm::vec3 Position;
    glm::vec3 Normal;
    glm::vec2 TexCoords;
    glm::vec3 Tangent;
    glm::vec3 Bitangent;
    // [新增]
    int m_BoneIDs[MAX_BONE_INFLUENCE];
    // [新增]
    float m_Weights[MAX_BONE_INFLUENCE];
};

struct Texture {
    unsigned int id;
    std::string type;
    std::string path;
};

class Mesh {
public:
    std::vector<Vertex> vertices;
    std::vector<unsigned int> indices;
    std::vector<Texture> textures;
    unsigned int VAO;

    Mesh(std::vector<Vertex> vertices, std::vector<unsigned int> indices, std::vector<Texture> textures)
    {
        this->vertices = vertices;
        this->indices = indices;
        this->textures = textures;

        setupMesh();
    }

    void Draw(Shader& shader)
    {
        unsigned int diffuseNr = 1;
        unsigned int specularNr = 1;
        unsigned int normalNr = 1;
        unsigned int heightNr = 1;
        for (unsigned int i = 0; i < textures.size(); i++)
        {
            glActiveTexture(GL_TEXTURE0 + i);

            std::string number;
            std::string name = textures[i].type;
            if (name == "texture_diffuse")
                number = std::to_string(diffuseNr++);
            else if (name == "texture_specular")
                number = std::to_string(specularNr++);
            else if (name == "texture_normal")
                number = std::to_string(normalNr++);
            else if (name == "texture_height")
                number = std::to_string(heightNr++);

            glUniform1i(glGetUniformLocation(shader.ID, (name + number).c_str()), i);
            glBindTexture(GL_TEXTURE_2D, textures[i].id);
        }

        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);

        glActiveTexture(GL_TEXTURE0);
    }

private:
    unsigned int VBO, EBO;

    void setupMesh()
    {
        glGenVertexArrays(1, &VAO);
        glGenBuffers(1, &VBO);
        glGenBuffers(1, &EBO);

        glBindVertexArray(VAO);
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

        // 顶点位置
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
        // 法线
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
        // uv纹理坐标
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));
        // tangent切线
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Tangent));
        // bitangent副切线
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Bitangent));
        // [新增] 骨骼ID (整数属性,必须用 glVertexAttribIPointer)
        glEnableVertexAttribArray(5);
        glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), (void*)offsetof(Vertex, m_BoneIDs));
        // [新增] 骨骼权重
        glEnableVertexAttribArray(6);
        glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, m_Weights));

        glBindVertexArray(0);
    }
};
#endif

model

#ifndef MODEL_H
#define MODEL_H

#include <glad/glad.h> 

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include "stb_image.h"
#include "gl_mesh.h"

#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
#include <vector>
#include <map>
#include <cassert> 


#define MAX_BONE_INFLUENCE 4

unsigned int TextureFromFile(const char* path, const std::string& directory, bool gamma = false);

// [����] Assimp ����ת glm ����
inline glm::mat4 aiMatrix4x4ToGlm(const aiMatrix4x4& from)
{
    glm::mat4 to;
    to[0][0] = from.a1; to[1][0] = from.b1; to[2][0] = from.c1; to[3][0] = from.d1;
    to[0][1] = from.a2; to[1][1] = from.b2; to[2][1] = from.c2; to[3][1] = from.d2;
    to[0][2] = from.a3; to[1][2] = from.b3; to[2][2] = from.c3; to[3][2] = from.d3;
    to[0][3] = from.a4; to[1][3] = from.b4; to[2][3] = from.c4; to[3][3] = from.d4;
    return to;
}

// [����] ������Ϣ��ID + ��ģ�Ϳռ䵽�����ռ��ƫ�ƾ���
struct BoneInfo
{
    int id;
    glm::mat4 offset;
};

// [����] �����ڵ㣬����㼶��ϵ�ͱ任
struct BoneNode
{
    std::string name;
    glm::mat4 originalTransform;   // ģ���ļ����ԭʼ���ر任
    glm::mat4 localTransform;      // ��ǰ���ر任���ɱ������޸ģ�
    std::vector<BoneNode> children;
};

class Model
{
public:
    std::vector<Texture> textures_loaded;
    std::vector<Mesh>    meshes;
    std::string directory;
    bool gammaCorrection;

    // [����] �����������
    std::map<std::string, BoneInfo> m_BoneInfoMap;
    int m_BoneCounter = 0;
    BoneNode m_RootNode;
    std::vector<glm::mat4> m_FinalBoneMatrices;

    Model(std::string const& path, bool gamma = false) : gammaCorrection(gamma)
    {
        loadModel(path);
        m_FinalBoneMatrices.resize(100, glm::mat4(1.0f)); // [����] Ԥ����100����������
    }

    void Draw(Shader& shader)
    {
        for (unsigned int i = 0; i < meshes.size(); i++)
            meshes[i].Draw(shader);
    }

    // [����]
    auto& GetBoneInfoMap() { return m_BoneInfoMap; }
    // [����]
    int& GetBoneCount() { return m_BoneCounter; }

    // [����] �������ƣ�����������������ת�Ƕȣ�pitch=x, yaw=y, roll=z������λ����
    void SetBoneRotation(const std::string& boneName, float pitch, float yaw, float roll)
    {
        BoneNode* node = FindBoneNode(&m_RootNode, boneName);
        if (!node) return;

        // ����ԭʼƽ�ƣ��滻��ת
        glm::vec3 pos = glm::vec3(node->originalTransform[3]);

        glm::mat4 rotX = glm::rotate(glm::mat4(1.0f), glm::radians(pitch), glm::vec3(1, 0, 0));
        glm::mat4 rotY = glm::rotate(glm::mat4(1.0f), glm::radians(yaw), glm::vec3(0, 1, 0));
        glm::mat4 rotZ = glm::rotate(glm::mat4(1.0f), glm::radians(roll), glm::vec3(0, 0, 1));

        node->localTransform = glm::translate(glm::mat4(1.0f), pos) * rotY * rotX * rotZ;
    }

    // [����] �Ӹ��ڵ㿪ʼ�ݹ�������й��������ձ任����
    void UpdateSkeleton()
    {
        UpdateSkeletonNode(&m_RootNode, glm::mat4(1.0f));
    }

    // [����] ��ȡ���չ����������飨������ɫ����
    const std::vector<glm::mat4>& GetFinalBoneMatrices() const { return m_FinalBoneMatrices; }

private:
    void loadModel(std::string const& path)
    {
        Assimp::Importer importer;
        const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);
        if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
        {
            std::cout << "ERROR::ASSIMP:: " << importer.GetErrorString() << std::endl;
            return;
        }
        directory = path.substr(0, path.find_last_of('/'));

        processNode(scene->mRootNode, scene);
        ReadNodeHierarchy(m_RootNode, scene->mRootNode); // [����] ��ȡ�����㼶
    }

    void processNode(aiNode* node, const aiScene* scene)
    {
        for (unsigned int i = 0; i < node->mNumMeshes; i++)
        {
            aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
            meshes.push_back(processMesh(mesh, scene));
        }
        for (unsigned int i = 0; i < node->mNumChildren; i++)
        {
            processNode(node->mChildren[i], scene);
        }
    }

    // [����] ���������ù���Ȩ��
    void SetVertexBoneData(Vertex& vertex, int boneID, float weight)
    {
        for (int i = 0; i < MAX_BONE_INFLUENCE; ++i)
        {
            if (vertex.m_BoneIDs[i] < 0)
            {
                vertex.m_Weights[i] = weight;
                vertex.m_BoneIDs[i] = boneID;
                break;
            }
        }
    }

    // [����] �� Assimp ��������ȡ����Ȩ����Ϣ
    void ExtractBoneWeightForVertices(std::vector<Vertex>& vertices, aiMesh* mesh, const aiScene* scene)
    {
        for (int boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
        {
            int boneID = -1;
            std::string boneName = mesh->mBones[boneIndex]->mName.C_Str();
            if (m_BoneInfoMap.find(boneName) == m_BoneInfoMap.end())
            {
                BoneInfo newBoneInfo;
                newBoneInfo.id = m_BoneCounter;
                newBoneInfo.offset = aiMatrix4x4ToGlm(mesh->mBones[boneIndex]->mOffsetMatrix);
                m_BoneInfoMap[boneName] = newBoneInfo;
                boneID = m_BoneCounter;
                m_BoneCounter++;
            }
            else
            {
                boneID = m_BoneInfoMap[boneName].id;
            }
            assert(boneID != -1);
            auto weights = mesh->mBones[boneIndex]->mWeights;
            int numWeights = mesh->mBones[boneIndex]->mNumWeights;

            for (int weightIndex = 0; weightIndex < numWeights; ++weightIndex)
            {
                int vertexId = weights[weightIndex].mVertexId;
                float weight = weights[weightIndex].mWeight;
                assert(vertexId <= (int)vertices.size());
                SetVertexBoneData(vertices[vertexId], boneID, weight);
            }
        }
    }

    Mesh processMesh(aiMesh* mesh, const aiScene* scene)
    {
        std::vector<Vertex> vertices;
        std::vector<unsigned int> indices;
        std::vector<Texture> textures;

        for (unsigned int i = 0; i < mesh->mNumVertices; i++)
        {
            Vertex vertex;
            glm::vec3 vector;
            vector.x = mesh->mVertices[i].x;
            vector.y = mesh->mVertices[i].y;
            vector.z = mesh->mVertices[i].z;
            vertex.Position = vector;

            if (mesh->HasNormals())
            {
                vector.x = mesh->mNormals[i].x;
                vector.y = mesh->mNormals[i].y;
                vector.z = mesh->mNormals[i].z;
                vertex.Normal = vector;
            }

            if (mesh->mTextureCoords[0])
            {
                glm::vec2 vec;
                vec.x = mesh->mTextureCoords[0][i].x;
                vec.y = mesh->mTextureCoords[0][i].y;
                vertex.TexCoords = vec;
                vector.x = mesh->mTangents[i].x;
                vector.y = mesh->mTangents[i].y;
                vector.z = mesh->mTangents[i].z;
                vertex.Tangent = vector;
                vector.x = mesh->mBitangents[i].x;
                vector.y = mesh->mBitangents[i].y;
                vector.z = mesh->mBitangents[i].z;
                vertex.Bitangent = vector;
            }
            else
                vertex.TexCoords = glm::vec2(0.0f, 0.0f);

            // [����] ��ʼ����������
            for (int j = 0; j < MAX_BONE_INFLUENCE; j++)
            {
                vertex.m_BoneIDs[j] = -1;
                vertex.m_Weights[j] = 0.0f;
            }

            vertices.push_back(vertex);
        }

        // [����] ��ȡ����Ȩ��
        ExtractBoneWeightForVertices(vertices, mesh, scene);

        for (unsigned int i = 0; i < mesh->mNumFaces; i++)
        {
            aiFace face = mesh->mFaces[i];
            for (unsigned int j = 0; j < face.mNumIndices; j++)
                indices.push_back(face.mIndices[j]);
        }

        aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
        std::vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
        textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());
        std::vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
        textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
        std::vector<Texture> normalMaps = loadMaterialTextures(material, aiTextureType_HEIGHT, "texture_normal");
        textures.insert(textures.end(), normalMaps.begin(), normalMaps.end());
        std::vector<Texture> heightMaps = loadMaterialTextures(material, aiTextureType_AMBIENT, "texture_height");
        textures.insert(textures.end(), heightMaps.begin(), heightMaps.end());

        return Mesh(vertices, indices, textures);
    }

    std::vector<Texture> loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName)
    {
        std::vector<Texture> textures;
        for (unsigned int i = 0; i < mat->GetTextureCount(type); i++)
        {
            aiString str;
            mat->GetTexture(type, i, &str);
            bool skip = false;
            for (unsigned int j = 0; j < textures_loaded.size(); j++)
            {
                if (std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
                {
                    textures.push_back(textures_loaded[j]);
                    skip = true;
                    break;
                }
            }
            if (!skip)
            {
                Texture texture;
                texture.id = TextureFromFile(str.C_Str(), this->directory);
                texture.type = typeName;
                texture.path = str.C_Str();
                textures.push_back(texture);
                textures_loaded.push_back(texture);
            }
        }
        return textures;
    }

    // [����] �ݹ��ȡ Assimp �ڵ�㼶������ BoneNode ��
    void ReadNodeHierarchy(BoneNode& dest, const aiNode* src)
    {
        dest.name = src->mName.C_Str();
        dest.originalTransform = aiMatrix4x4ToGlm(src->mTransformation);
        dest.localTransform = dest.originalTransform;
        dest.children.resize(src->mNumChildren);
        for (unsigned int i = 0; i < src->mNumChildren; i++)
        {
            ReadNodeHierarchy(dest.children[i], src->mChildren[i]);
        }
    }

    // [����] �����Ʋ��ҹ����ڵ�
    BoneNode* FindBoneNode(BoneNode* node, const std::string& name)
    {
        if (node->name == name) return node;
        for (auto& child : node->children)
        {
            BoneNode* found = FindBoneNode(&child, name);
            if (found) return found;
        }
        return nullptr;
    }

    // [����] �ݹ����������ձ任���󣺸��任 �� ���ر任 �� Offset
    void UpdateSkeletonNode(BoneNode* node, const glm::mat4& parentTransform)
    {
        glm::mat4 globalTransform = parentTransform * node->localTransform;
        if (m_BoneInfoMap.find(node->name) != m_BoneInfoMap.end())
        {
            int boneId = m_BoneInfoMap[node->name].id;
            m_FinalBoneMatrices[boneId] = globalTransform * m_BoneInfoMap[node->name].offset;
        }
        for (auto& child : node->children)
        {
            UpdateSkeletonNode(&child, globalTransform);
        }
    }
};

unsigned int TextureFromFile(const char* path, const std::string& directory, bool gamma)
{
    std::string filename = std::string(path);
    filename = directory + '/' + filename;

    unsigned int textureID;
    glGenTextures(1, &textureID);

    int width, height, nrComponents;
    unsigned char* data = stbi_load(filename.c_str(), &width, &height, &nrComponents, 0);
    if (data)
    {
        GLenum format;
        if (nrComponents == 1)
            format = GL_RED;
        else if (nrComponents == 3)
            format = GL_RGB;
        else if (nrComponents == 4)
            format = GL_RGBA;

        glBindTexture(GL_TEXTURE_2D, textureID);
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

        stbi_image_free(data);
    }
    else
    {
        std::cout << "Texture failed to load at path: " << path << std::endl;
        stbi_image_free(data);
    }

    return textureID;
}
#endif

nanosuit的人物模型,我是从learnopengl拿来的;以及stb_img_write.h,stb_image.h等都可以从网上找到;