关于鸣潮启动器450张图片杂谈—从代码分析为何使用帧动画
前言
在鸣潮启动器的目录下
Wuthering Waves\kr_game_cache\animate_bg\99de27ae82e3c370286fba14c4fcb699
打开该目录发现有450张图片,不难看出启动器的背景动画是由这450张图片不断切换实现的
qt框架
从动态库能很明显的看出启动器是用qt5写的,而使用qt实现动态背景图的方式主要有以下几种:1.帧动画,也是官方启动器选择的方式 2.使用ffmpeg等开源音视频解码库对视频文件进行解码,3.使用外部解码软件,4.使用gif动图
帧动画
首先来看第一个解决方案,也是最简单,效果也不错的方案,以下是代码,非常简单一共也就十几行,直接一个定时器不断切换背景图片路径就行了
static int index = 0;
AnimatedBackground::AnimatedBackground(QWidget *parent): QWidget{parent}
{this->setFixedSize(1280,760);m_timer = new QTimer(this);connect(m_timer,&QTimer::timeout,this,[&]()mutable{index%=450;index++;QString path = "D:\\Wuthering Waves\\kr_game_cache\\animate_bg\\99de27ae82e3c370286fba14c4fcb699\\home_"+QString::number(index)+".jpg";m_currentBackground.load(path);update();});m_timer->start(1000/33);
}void AnimatedBackground::paintEvent(QPaintEvent *event)
{QPainter painter(this);painter.drawPixmap(rect(),m_currentBackground);
}
来看效果:
使用ffmpeg软解码视频
qt框架自己并没有附带解码器,要实现播放视频需要解码库或者解码软件,这里使用开源的ffmpeg对视频进行解码
首先要将ffmpeg添加到自己的项目:
- 下载源码并编译(略)
- 将编译好的库文件和头文件添加到项目(略)
- 编写Cmake/qmake 文件将库链接到项目(略)
实现方案:
由于解码视频是需要时间的,如果等视频流所有的帧都解码完才显示会有几秒左右的延迟,要想实现第一个方案打开就有动画效果,需要解码和显示同时进行
需要一个队列对帧数据进行缓存,所以选择基于生产者-消费者的设计模式的线程模型 ,以下是原理图:
涉及多线程的的话,当然需要锁啦 先封装个锁:
semaphore.h
#ifndef SEMAPHORE_H
#define SEMAPHORE_H#include <atomic>
#include <condition_variable>
#include <mutex>class Semaphore
{
public:explicit Semaphore(int i = 0) {m_semaphore.store(i < 0 ? 0 : i);}Semaphore(const Semaphore &) = delete;Semaphore& operator=(const Semaphore &) = delete;void acquire(int i = 1) {if (i <= 0) return;std::unique_lock<std::mutex> lock(m_mutex);if (m_semaphore.load() < i) {m_conditionVar.wait(lock);}m_semaphore.fetch_sub(i);}bool tryAcquire(int i = 1) {if (i <= 0) return false;if (m_semaphore.load() >= i) {m_semaphore.fetch_sub(i);return true;} else return false;}void release(int i = 1) {if (i <= 0) return;m_semaphore.fetch_add(i);m_conditionVar.notify_one();}int available() const {return m_semaphore.load();}private:std::condition_variable m_conditionVar;std::atomic_int m_semaphore;std::mutex m_mutex;
};#endif
实现缓存队列bufferqueue.h
#ifndef BUFFERQUEUE_H
#define BUFFERQUEUE_H#ifdef DEBUG_OUTPUT
#include <iostream>
#endif#include "semaphore.h"
#include <vector>template <class T> class BufferQueue
{
public:BufferQueue(int bufferSize = 100) {setBufferSize(bufferSize);}~BufferQueue() {init();std::vector<T>().swap(m_bufferQueue);}void setBufferSize(int bufferSize) {m_bufferSize = bufferSize;m_bufferQueue = std::vector<T>(bufferSize);m_useableSpace.acquire(m_useableSpace.available());m_freeSpace.release(m_bufferSize - m_freeSpace.available());m_front = m_rear = 0;}void enqueue(const T &element) {
#ifdef DEBUG_OUTPUTstd::cout << "[freespace " << m_freeSpace.available()<< "] --- [useablespace " << m_useableSpace.available() << "]" << std::endl;
#endifm_freeSpace.acquire();m_bufferQueue[m_front++ % m_bufferSize] = element;m_useableSpace.release();}T dequeue() {
#ifdef DEBUG_OUTPUTstd::cout << "[freespace " << m_freeSpace.available()<< "] --- [useablespace " << m_useableSpace.available() << "]" << std::endl;
#endifm_useableSpace.acquire();T element = m_bufferQueue[m_rear++ % m_bufferSize];m_freeSpace.release();return element;}/*** @brief tryDequeue* @note 尝试获取一个元素,并且在失败时不会阻塞调用线程* @return 成功返回对应T元素,失败返回默认构造的T元素*/T tryDequeue() {T element;bool success = m_useableSpace.tryAcquire();if (success) {element = m_bufferQueue[m_rear++ % m_bufferSize];m_freeSpace.release();}return element;}void init() {m_useableSpace.acquire(m_useableSpace.available());m_freeSpace.release(m_bufferSize - m_freeSpace.available());m_front.store(0);m_rear.store(0);}private:// -1 +1// [free space] -> [useable space]Semaphore m_freeSpace;Semaphore m_useableSpace;std::atomic_int m_rear;std::atomic_int m_front;std::vector<T> m_bufferQueue;int m_bufferSize;
};#endif
封装视频解码类:
videodecoder.h
class VideoDecoder : public QThread
{Q_OBJECTpublic:VideoDecoder(QObject *parent = nullptr);~VideoDecoder();void stop();void open(const QString &filename);int fps() const { return m_fps; }int width() const { return m_width; }int height() const { return m_height; }QImage currentFrame();signals:void resolved();void finish();protected:void run();private:void demuxing_decoding();bool m_runnable = true;QMutex m_mutex;QString m_filename;BufferQueue<QImage> m_frameQueue;int m_fps, m_width, m_height;
};
videodecoder.cpp
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
#include <QApplication>
#include <QHBoxLayout>
#include <QMimeData>
#include <QPushButton>
#include <QPainter>
#include <QTimer>
#include <QDebug>VideoDecoder::VideoDecoder(QObject *parent): QThread (parent)
{}VideoDecoder::~VideoDecoder()
{stop();
}void VideoDecoder::stop()
{//必须先重置信号量m_frameQueue.init();m_runnable = false;wait();
}void VideoDecoder::open(const QString &filename)
{stop();m_mutex.lock();m_filename = filename;m_runnable = true;m_mutex.unlock();start();
}QImage VideoDecoder::currentFrame()
{static QImage image = QImage();image = m_frameQueue.tryDequeue();return image;
}void VideoDecoder::run()
{demuxing_decoding();
}void VideoDecoder::demuxing_decoding()
{AVFormatContext *formatContext = nullptr;AVCodecContext *codecContext = nullptr;AVCodec *videoDecoder = nullptr;AVStream *videoStream = nullptr;int videoIndex = -1;//打开输入文件,并分配格式上下文avformat_open_input(&formatContext, m_filename.toStdString().c_str(), nullptr, nullptr);avformat_find_stream_info(formatContext, nullptr);//找到视频流的索引videoIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if (videoIndex < 0) {qDebug() << "Has Error: line =" << __LINE__;return;}videoStream = formatContext->streams[videoIndex];if (!videoStream) {qDebug() << "Has Error: line =" << __LINE__;return;}videoDecoder = avcodec_find_decoder(videoStream->codecpar->codec_id);if (!videoDecoder) {qDebug() << "Has Error: line =" << __LINE__;return;}codecContext = avcodec_alloc_context3(videoDecoder);if (!codecContext) {qDebug() << "Has Error: line =" << __LINE__;return;}avcodec_parameters_to_context(codecContext, videoStream->codecpar);if (!codecContext) {qDebug() << "Has Error: line =" << __LINE__;return;}avcodec_open2(codecContext, videoDecoder, nullptr);//打印相关信息av_dump_format(formatContext, 0, "format", 0);fflush(stderr);m_fps = videoStream->avg_frame_rate.num / videoStream->avg_frame_rate.den;m_width = codecContext->width;m_height = codecContext->height;emit resolved();SwsContext *swsContext = sws_getContext(m_width, m_height, codecContext->pix_fmt, m_width, m_height, AV_PIX_FMT_RGB24,SWS_BILINEAR, nullptr, nullptr, nullptr);//分配并初始化一个临时的帧和包AVPacket *packet = av_packet_alloc();AVFrame *frame = av_frame_alloc();packet->data = nullptr;packet->size = 0;//读取下一帧while (m_runnable && av_read_frame(formatContext, packet) >= 0) {if (packet->stream_index == videoIndex) {//发送给解码器int ret = avcodec_send_packet(codecContext, packet);while (ret >= 0) {//从解码器接收解码后的帧ret = avcodec_receive_frame(codecContext, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;else if (ret < 0) goto Run_End;int dst_linesize[4];uint8_t *dst_data[4];av_image_alloc(dst_data, dst_linesize, m_width, m_height, AV_PIX_FMT_RGB24, 1);sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height, dst_data, dst_linesize);QImage image = QImage(dst_data[0], m_width, m_height, QImage::Format_RGB888).copy();av_freep(&dst_data[0]);m_frameQueue.enqueue(image);av_frame_unref(frame);}}av_packet_unref(packet);}Run_End:m_fps = m_width = m_height = 0;if (frame) av_frame_free(&frame);if (packet) av_packet_free(&packet);if (swsContext) sws_freeContext(swsContext);if (codecContext) avcodec_free_context(&codecContext);if (formatContext) avformat_close_input(&formatContext);
}
显示:
AnimatedBackground::AnimatedBackground(QWidget *parent): QWidget(parent)
{this->setFixedSize(1280,760);m_timer = new QTimer(this);connect(m_timer, &QTimer::timeout, this, [this](){m_currentFrame = m_decoder->currentFrame();update();});m_decoder = new VideoDecoder(this);connect(m_decoder, &VideoDecoder::resolved, this, [this]() {m_timer->start(1000 / m_decoder->fps());});m_decoder->open(":/video/1.mp4");
}
void AnimatedBackground::paintEvent(QPaintEvent *event)
{Q_UNUSED(event);QPainter painter(this);if (!m_currentFrame.isNull())painter.drawImage(rect(), m_currentFrame);
}
效果和第一个方案是一模一样的这里就不做展示了,总之,软解码视频方案比帧动画多了几百行代码不说,ffmpeg这个库也是比较难写的,由于是c语言风格,写之前还需要一定的时间费脑子去阅读文档,实现效果和帧动画没什么区别,内存上也没减少多少虽然图片经压缩视频后大小减小,但难蹦的是库文件的大小比450张图片还大,总之就是十分吃力不讨好。
使用外部解码软件
qt的QMediaPlayer可以使用外部的解码器进行解码,从而实现视频播放,但是不能保证用户是否下载了解码器,要绑定安装的话是十分流氓的行为,而且不开源的软件商用也是要钱的,直接用也是会有商业纠纷,这种方案不必多说
播放gif
效果差,糊的一批的同时帧率还低
总之
综合看下来,帧动画是最优的解决方案,简单且高效,软解码不说前期可能遇到的环境问题不说,代码也是多了几百行,给自己多加了一两天的工作量,内存空间上不但没有因为图片压缩成视频减小空间,反而因为添加动态库比原先还大,是十分吃力且不讨好的行为。代码的最终目的是为了服务于产品的,不管哪种代码,你只要能达到最终的效果,那就是好代码
另外分享一个有趣的:windows的开机动画也是用图标字体一帧一帧拼起来的