使用 C++/OpenCV 创建动态流星雨特效 (实时动画)

发布于:2025-06-08 ⋅ 阅读:(15) ⋅ 点赞:(0)

使用 C++/OpenCV 创建动态流星雨特效 (实时动画)

本文将指导你如何使用 C++ 和 OpenCV 创建一个实时的流星雨动画。与之前为静态图片添加特效不同,这次我们将利用循环和 imshow 来生成一个持续不断的动态效果,流星会在背景上划过并消失。
在这里插入图片描述

实现思路

我们将创建一个主循环,每一轮循环都代表动画的一帧。

  1. 管理流星对象: 我们需要一个容器(如 std::vector)来存储屏幕上所有活跃的流星。
  2. 定义流星类: 创建一个 Meteor 类或结构体,它不仅包含外观属性(颜色、粗细),还包含动画属性,如当前位置、速度和生命周期。
  3. 主循环:
    • 生成流星: 在每一帧,有一定几率在屏幕外随机生成新的流星。
    • 更新状态: 更新每个流星的位置(位置 += 速度)。
    • 绘制画面: 在每一帧开始时,先加载原始背景图,然后将所有更新后的流星绘制到这张图上。
    • 移除消失的流星: 当流星飞出画面或其生命周期结束时,将其从容器中移除。
    • 显示: 使用 cv::imshow() 显示绘制好的一帧画面。
    • 循环与退出: 重复以上步骤,直到用户按下退出键(如 ‘q’ 或 ESC)。

完整示例代码 👨‍💻

这个程序会加载一张背景图,并在其上实时渲染动态的流星雨。

dynamic_meteor_shower.cpp

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <random>

// 使用一个类来更好地管理流星的状态
class Meteor {
public:
    cv::Point2f pos;       // 当前位置 (使用浮点数以获得平滑移动)
    cv::Vec2f velocity;    // 速度 (方向和速率)
    cv::Scalar color;      // 颜色
    int length;            // 尾巴长度
    int thickness;         // 线条粗细

    Meteor(int screenWidth, int screenHeight) {
        // 1. 初始化位置和速度
        // 随机决定从顶部还是左/右侧出现
        if (rand() % 2 == 0) {
            // 从顶部随机位置出现
            pos = cv::Point2f(rand() % screenWidth, -50.0f);
            velocity = cv::Vec2f(2.0f + (rand() % 4), 10.0f + (rand() % 10)); // 向右下
        } else {
            // 从左侧或右侧出现
            if (rand() % 2 == 0) {
                pos = cv::Point2f(-50.0f, rand() % (screenHeight / 2));
                velocity = cv::Vec2f(5.0f + (rand() % 10), 5.0f + (rand() % 5)); // 向右下
            } else {
                pos = cv::Point2f(screenWidth + 50.0f, rand() % (screenHeight / 2));
                velocity = cv::Vec2f(-(5.0f + (rand() % 10)), 5.0f + (rand() % 5)); // 向左下
            }
        }
        
        // 2. 初始化外观
        int brightness = 180 + rand() % 76; // 较亮的颜色
        color = cv::Scalar(brightness, brightness, 255); // 淡蓝色或白色
        length = 20 + rand() % 80;
        thickness = 1 + rand() % 2;
    }

    // 更新流星位置
    void update() {
        pos += cv::Point2f(velocity[0], velocity[1]);
    }

    // 绘制流星
    void draw(cv::Mat& canvas) const {
        // 计算尾巴的起点
        cv::Point tail_start = pos - cv::Point2f(velocity[0], velocity[1]) * (float)(length / cv::norm(velocity));
        // 绘制流星的主体(一条亮线)
        cv::line(canvas, pos, tail_start, color, thickness, cv::LINE_AA);
        
        // 绘制一个更亮的头部
        cv::circle(canvas, pos, thickness, cv::Scalar(255, 255, 255), -1, cv::LINE_AA);
    }
    
    // 检查流星是否已经飞出屏幕
    bool isOffscreen(int screenWidth, int screenHeight) const {
        return (pos.x < -100 || pos.x > screenWidth + 100 || pos.y > screenHeight + 100);
    }
};

int main() {
    // 1. 加载背景图片
    cv::Mat background = cv::imread("background.jpg");
    if (background.empty()) {
        std::cerr << "错误: 无法加载背景图片 'background.jpg'!" << std::endl;
        return -1;
    }

    int width = background.cols;
    int height = background.rows;

    std::vector<Meteor> meteors;
    const int MAX_METEORS = 30; // 屏幕上最多同时存在的流星数量

    cv::namedWindow("Dynamic Meteor Shower", cv::WINDOW_AUTOSIZE);

    while (true) {
        // 2. 每一帧都从原始背景图开始绘制,避免留下残影
        cv::Mat frame = background.clone();

        // 3. 有一定几率生成新的流星
        if (meteors.size() < MAX_METEORS && rand() % 5 == 0) {
            meteors.push_back(Meteor(width, height));
        }

        // 4. 更新和绘制所有流星
        for (auto& meteor : meteors) {
            meteor.update();
            meteor.draw(frame);
        }

        // 5. 移除飞出屏幕的流星
        // 使用 C++11 的 remove_if 和 lambda 表达式,非常高效
        meteors.erase(
            std::remove_if(meteors.begin(), meteors.end(), 
                [width, height](const Meteor& m) {
                    return m.isOffscreen(width, height);
                }),
            meteors.end()
        );

        // 6. 显示最终合成的帧
        cv::imshow("Dynamic Meteor Shower", frame);

        // 7. 检测用户输入,如果按下 'q' 键或 ESC 键则退出循环
        char key = (char)cv::waitKey(25); // 等待25毫秒
        if (key == 'q' || key == 27) {
            break;
        }
    }

    cv::destroyAllWindows();
    return 0;
}

CMakeLists.txt

编译文件保持不变。

cmake_minimum_required(VERSION 3.10)
project(DynamicMeteorShower)

find_package(OpenCV REQUIRED)

include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(dynamic_meteor_shower dynamic_meteor_shower.cpp)
target_link_libraries(dynamic_meteor_shower ${OpenCV_LIBS})

如何编译和运行

  1. dynamic_meteor_shower.cppCMakeLists.txt 保存到同一个目录下。
  2. 准备一张背景图片,命名为 background.jpg 并放在同一目录。
  3. 编译和运行:
    mkdir -p build && cd build
    cmake ..
    make
    ./dynamic_meteor_shower
    

运行后,你会看到一个窗口,其中有流星不断地从屏幕外划入,飞过背景图,然后消失。按 q 键或 Esc 键可以关闭程序。


核心代码解析

  • Meteor:

    • 构造函数 Meteor(width, height) 负责在屏幕外随机生成流星的初始位置和速度。
    • update() 方法简单地将速度加到当前位置上,实现移动。
    • draw(canvas) 方法在给定的图像(canvas)上绘制流星。它通过计算一个尾巴起点来画出一条线,并在线的头部画一个更亮的圆点,模拟流星头。
    • isOffscreen() 用于判断流星是否已经飞出可视区域,以便我们将其移除。
  • while 循环:

    • cv::Mat frame = background.clone();: 这是实现动画的关键。每一帧我们都重新复制背景图,确保上一帧的流星被清除,只绘制当前帧的流星位置。
    • 生成: if (meteors.size() < MAX_METEORS && rand() % 5 == 0) 控制流星的生成速率,既不会太多也不会太少。
    • 更新与绘制: 遍历 meteors 向量,调用每个对象的 update()draw() 方法。
    • 移除: meteors.erase(std::remove_if(...)) 是从 std::vector 中安全高效地移除满足特定条件(在这里是 isOffscreen)的元素的标准方法。
    • cv::waitKey(25): 这个函数不仅等待用户按键,也为动画提供了大约 40FPS (1000ms / 25ms = 40) 的帧率。你可以调整这个值来改变动画的速度。