关于鸣潮启动器450张图片杂谈—从代码分析为何使用帧动画

关于鸣潮启动器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的开机动画也是用图标字体一帧一帧拼起来的

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/402513.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024千元以下蓝牙耳机有哪些推荐?四款年度性价比蓝牙耳机推荐

2024年&#xff0c;蓝牙耳机市场再次迎来了新的发展机遇与挑战&#xff0c;在众多的蓝牙耳机中&#xff0c;千元以下的产品因其较高的性价比而备受消费者青睐&#xff0c;那么面对琳琅满目的产品&#xff0c;2024千元以下蓝牙耳机有哪些推荐&#xff1f;接下来下面&#xff0c;…

【TM1638不能成功读回按键值】

8led8按键8数码管。主函数调用TM1638_ReadData2&#xff0c;打印了返回值&#xff0c;无论是否按键&#xff0c;都一直打印255&#xff0c;为什么全是1&#xff0c;看来读数据函数有问题啊。 u8 TM1638_ReadData2(void) {uint8_t i;uint8_t temp0x00;TM1638_DIOModeInput();/…

【机器学习西瓜书学习笔记——半监督学习】

机器学习西瓜书学习笔记【第十三章】 第十三章 半监督学习13.1 未标记样本13.2 生成式方法13.3 半监督 S V M SVM SVM基本思想优点和注意事项适用场景 13.4 图半监督标签传播算法多类标签传播算法Label PropagationLabel Spreading 13.5 基于分歧的方法数据视图协同训练 13.6 半…

CI/CD 自动化:最大限度地提高极狐GitLab 群组的“部署冻结”影响

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门面向中国程序员和企业提供企业级一体化 DevOps 平台&#xff0c;用来帮助用户实现需求管理、源代码托管、CI/CD、安全合规&#xff0c;而且所有的操作都是在一个平台上进行&#xff0c;省事省心省钱。可以一键安装极狐GitL…

基于LangChain手工测试用例转接口自动化测试生成工具!

接口自动化测试用例是一个老生常谈的问题&#xff0c;在未引入人工智能之前&#xff0c;也有非常多的生成方案&#xff0c;比如如下所示&#xff0c;通过har生成接口自动化测试用例&#xff1a; 但是以上的生成方式依然是有一些弊端&#xff0c;比如 har 本身虽然能表述一定的接…

SqlSugar详解-国产ORM框架

ORM (Object-Relational Mapping) 概念 ORM 是一种程序技术&#xff0c;用于将关系型数据库中的数据映射到对象上。 主要目的是简化数据库操作&#xff0c;使得开发人员可以像操作对象一样来操作数据库。 原理 数据表与类的映射&#xff1a;数据库中的表对应为类。 记录与对象…

C++竞赛初阶L1-11-第五单元-for循环(25~26课)524: T454437 幂的末尾

题目内容 幂 ab 的末 3 位数是多少&#xff1f; 输入格式 两个正整数 a&#xff0c;b。1≤a≤100&#xff0c;1≤b≤10000。 输出格式 从高位到低位输出幂的末三位数字&#xff0c;中间无分隔符。若幂本身不足三位&#xff0c;在前面补零。 样例 1 输入 2 3 样例 2 输入…

django常用的组合搜索组件

文章目录 django常用的组合搜索组件快速使用配置信息1. 视图函数2. 前端模板3. css样式 代码实现 django常用的组合搜索组件 在项目开发中&#xff0c;如果有大量数据就要用到组合搜索&#xff0c;通过组合搜索对大块内容进行分类筛选。 快速使用 三步走&#xff1a;&#xf…

智慧社区新视界:EasyCVR视频汇聚平台下的数字化治理实践

在当今科技飞速发展的时代&#xff0c;“数字城市智慧社区”这个概念正逐渐走进我们的生活。那么&#xff0c;数字城市智慧社区到底是什么样子的呢&#xff1f; 随着城市化的不断推进&#xff0c;数字城市建设已成为提升城市管理效率、改善居民生活质量的重要手段。智慧社区作…

软件工程概述(上)

1、软件的概念、特点和分类 要了解软件工程&#xff0c;首先让我们重新认识一下软件。如今可以说是一个软件定义一切的时代&#xff0c;虽然人工智能发展的如火如荼&#xff0c;但究其本质&#xff0c;核心还是软件。那么&#xff0c;如何给软件下一个定义呢&#xff1f;软件又…

B站搜索建库架构优化实践

前言 搜索是B站的重要基础功能&#xff0c;需要对包括视频、评论、图文等海量的站内优质资源建立索引&#xff0c;处理来自用户每日数亿的检索请求。离线索引数据的正确、高效产出是搜索业务的基础。我们在这里分享搜索离线架构整体的改造实践&#xff1a;从周期长&#xff0c;…

Transformer架构;Encoder-Decoder;Padding Mask;Sequence Mask;

目录 Transformer架构 Transformer架构的主要组成部分: 简单举例说明输入和输出: Encoder-Decoder 编码器/解码器组成 6、位置前馈网络(Position-wise Feed-Forward Networks) 7、残差连接和层归一化 10、掩码Mask 10.1 Padding Mask 10.2 Sequence Mask 为什么…

二.PhotoKit - 相册权限(彻底读懂权限管理)

引言 用户的照片和视频算是用户最私密的数据之一&#xff0c;由于内置的隐私保护功能&#xff0c;APP只有在用户明确授权的前提下才能访问用户的照片库。从iOS14 开始&#xff0c;PhotoKit进一步增强了用户的隐私控制&#xff0c;用户可以选择指定的照片或者视频资源的访问权限…

java 面试 PDF 资料整理

“尊贵的求知者&#xff0c;作者特此献上精心编纂的Java面试宝典PDF&#xff0c;这份资料凝聚了无数面试精华与实战经验&#xff0c;是通往Java技术殿堂的钥匙。若您渴望在Java编程的求职之路上稳健前行&#xff0c;只需轻轻一点&#xff0c;完成这象征支持与认可的一键三联&am…

解决麒麟 V10 SP1 升级 Python 后 Yum 不可用问题

目录 一、前提概要 二、解决办法 1、卸载原有的 python 2、安装 Python 3.7.9 rpm 3、安装一系列 yum 相关 rpm 4、rpm 包下载 一、前提概要 在部署 gaussDB 的时候&#xff0c;安装代理时要求 python 版本满足 3.7.9&#xff0c;但已安装的麒麟 V10 内集成的 python 版…

docker安装es8和kibana

es8、kibana安装、ik分词器使用 1 拉取镜像2 创建网络3 安装ES3.1 启动临时的ES3.2 运行es3.3 修改用户密码 4 安装kibana4.1 启动临时的kibana4.2 修改配置文件4.3 重启创建并启动kibana容器4.4 调用5601端口&#xff08;我映射的是8082&#xff09; 5 安装ik分词器&#xff0…

Linux学习——文本处理工具与正则表达式

目录 一&#xff0c;grep 1&#xff0c;grep介绍 2&#xff0c;grep的常用选项 3&#xff0c;grep使用演示 1&#xff0c;基本使用 直接查找字符串&#xff1a; 使用选项 2&#xff0c;使用正则表达式进行匹配 1&#xff0c;正则表达式介绍 2&#xff0c;使用范例 二&…

servlet的执行顺序

执行的时候Tomcat先初始化 然后调用 server 根据server来回调请求方式下面会追入源码解释 package com.haogu.servlet;import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.…

Zookeeper的在Ubuntu20.04上的集群部署

安装资源 官方安装包下载地址&#xff1a;https://zookeeper.apache.org/releases.html 懒得找版本的可以移步下载zookeeper3.84稳定版本&#xff1a; https://download.csdn.net/download/qq_43439214/89646735 安装方法 创建安装路径&&解压安装包 # 创建路径 m…

STM32总线和时钟树(速记版)

一、存储器结构 1.1 STM32内存结构 型号说明 以STM32F103RBT6这个型号的芯片为例&#xff0c;该型号的组成为7个部分&#xff0c;其命名规则如下&#xff1a; ST 是公司名&#xff0c;意法半导体。M 代表Cortex-M内核。32 代表32位微控制器。 F103 是芯片系列。 R 代表引脚数…