using System; using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; using LibAPNG; public class APNGPlayer : MonoBehaviour { /// /// 图片来源 /// public enum ImageSource { FromStreamingAssets, FromFile, FromHttp, } /// /// Texture使用模式,单张或多张 /// 单张表示每帧动画都在一张Texture上绘制 /// 多张表示会为每帧动画单独创建一张Texture /// public enum TextureMode { SingleTexture, MultiTexture, } /// /// 加载状态 /// public enum LoadState { UNLOADED,//未加载 LOADING,//加载中 PROCESSING,//处理中 READY,//准备完成 ERROR,//错误 } /// /// 播放状态 /// public enum PlayState { STOPED,//停止 PLAYING,//播放 PAUSED,//暂停 } public class APNGFrame { //当前帧索引 public int index; public Frame frame; //当前帧图像数据 public Color32[] pixels; //当前帧持续时间 public float duration; //指定下一帧绘制之前对缓冲区的操作 public DisposeOps disposeOp; //指定绘制当前帧之前对缓冲区的操作 public BlendOps blendOp; //当前帧像素宽 public uint width; //当前帧像素高 public uint height; //当前帧x方向像素偏移 public uint xOffset; //当前帧y方向像素偏移 public uint yOffset; //当前帧Texture public Texture2D texture; public APNGFrame Clone() { var result = new APNGFrame(); result.index = this.index; result.frame = this.frame; result.pixels = this.pixels; result.duration = this.duration; result.disposeOp = this.disposeOp; result.blendOp = this.blendOp; result.width = this.width; result.height = this.height; result.xOffset = this.xOffset; result.yOffset = this.yOffset; result.texture = this.texture; return result; } } class ImagePixels { private uint mWidth; public uint width { get => mWidth; } private uint mHeight; public uint height { get => mHeight; } private Color32[] mPixels; public Color32[] pixels { get => mPixels; } public ImagePixels(uint width, uint height) { mWidth = width; mHeight = height; Clear(); } /// /// 清除所有像素点 /// public void Clear() { mPixels = new Color32[mWidth * mHeight]; } /// /// 清除一个矩形区域 /// /// 水平方向开始像素,从左到右顺序 /// 垂直方向开始像素,从下到上顺序 /// 像素宽 /// 像素高 public void ClearRect(uint x, uint y, uint width, uint height) { //var startIndex = y * mWidth + x; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { var index = (y + j) * mWidth + x + i; var color = mPixels[index]; color.r = 0; color.g = 0; color.b = 0; color.a = 0; } } } /// /// 放入像素点数据 /// /// 像素点数据 /// 水平方向开始像素,从左到右顺序 /// 垂直方向开始像素,从下到上顺序 /// 像素宽 /// 像素高 public void SetPixels(Color32[] pixels, uint x, uint y, uint width, uint height) { for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { var index = (y + j) * mWidth + x + i; mPixels[index] = pixels[j * width + i]; } } } /// /// 放入像素点数据 /// /// public void SetPixels(Color32[] pixels) { SetPixels(pixels, 0, 0, mWidth, mHeight); } /// /// 获取像素点数据 /// /// 水平方向开始像素,从左到右顺序 /// 垂直方向开始像素,从上到下顺序 /// 像素宽 /// 像素高 /// 像素点数据 public Color32[] GetPixels(uint x, uint y, uint width, uint height) { var result = new Color32[width * height]; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { var index = (y + j) * mWidth + x + i; result[j * width + i] = mPixels[index]; } } return result; } /// /// 获取像素点数据 /// /// 像素点数据 public Color32[] GetPixels() { return GetPixels(0, 0, mWidth, mHeight); } } [Tooltip("APNG图片加载路径")] public string imagePath; [Tooltip("APNG图片来源")] public ImageSource imageSource; [Tooltip("Texture使用模式")] public TextureMode textureMode = TextureMode.MultiTexture; [Tooltip("指定APNG图像所需要赋值的Material")] public List materials = new List(); [Tooltip("指定APNG图像所需要赋值的RawImage")] public List rawImages = new List(); [Tooltip("是否随脚本启动执行")] public bool runOnStart = true; [Tooltip("是否自动播放,为true则加载完成后立即开始播放,为false则需手动调用Play()才开始播放")] public bool autoPlay = true; [Tooltip("播放速度倍率")] [Min(0.1f)] public float playSpeed = 1.0f; [Tooltip("动画循环播放次数,0表示无限制")] [Min(0)] public int maxLoopCount = 0; private LoadState mLoadState = LoadState.UNLOADED; public bool isUnloaded { get { return mLoadState == LoadState.UNLOADED; } } public bool isLoading { get { return mLoadState == LoadState.LOADING; } } public bool isProcessing { get { return mLoadState == LoadState.PROCESSING; } } public bool isReady { get { return mLoadState == LoadState.READY; } } public bool isError { get { return mLoadState == LoadState.ERROR; } } private PlayState mPlayState = PlayState.STOPED; public bool isStoped { get { return mPlayState == PlayState.STOPED; } } public bool isPlaying { get { return mPlayState == PlayState.PLAYING; } } public bool isPaused { get { return mPlayState == PlayState.PAUSED; } } private APNG mApng; private List mFrames = new List(); private uint mWidth; public uint imageWidth { get { return mWidth; } } private uint mHeight; public uint imageHeight { get { return mHeight; } } private APNGFrame mPrevFrame; private Texture2D mTexture; [Obsolete] public Texture2D texture { get { return mTexture; } } public Texture2D currentTexture { get { return mTexture; } } private ImagePixels mImagePixels; private float mLastTime = 0.0f; private int mCurrentFrameIndex = -1; public int currentFrameIndex { get { return mCurrentFrameIndex; } } public int framesNumber { get { return mFrames.Count; } } private int mLoopCount = 0; public delegate void OnReady(APNGPlayer player); public delegate void OnError(APNGPlayer player, string error); public delegate void OnChanged(APNGPlayer player, int frameIndex, APNGFrame frame); public event OnReady onReady; public event OnError onError; public event OnChanged onChanged; // Start is called before the first frame update void Start() { if (runOnStart) { Run(); } } // Update is called once per frame void Update() { if (isReady && isPlaying) { checkNextFrame(); } } /// /// 开始运行 /// public void Run() { if (imagePath == null) { Debug.LogWarning("The url is null, can't call Run() method."); return; } if (isUnloaded || isError) StartCoroutine(load()); else Debug.LogWarning("This player load state is " + mLoadState + ", can't call Run() method."); } //加载图片并处理 private IEnumerator load() { mLoadState = LoadState.LOADING; Uri uri; if (imageSource == ImageSource.FromStreamingAssets) uri = new Uri(Path.Combine(Application.streamingAssetsPath, imagePath)); else uri = new Uri(imagePath); using (UnityWebRequest www = UnityWebRequest.Get(uri)) { yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { string error = "Get " + imagePath + " error: " + www.error; Debug.LogError(error); mLoadState = LoadState.ERROR; onError?.Invoke(this, error); yield break; } //开始数据处理 mLoadState = LoadState.PROCESSING; try { //解析APNG图片数据 mApng = new APNG(www.downloadHandler.data); } catch (System.Exception e) { Debug.LogError(e.Message); mLoadState = LoadState.ERROR; onError?.Invoke(this, e.Message); yield break; } yield return null; //获取图片宽高 mWidth = (uint)mApng.IHDRChunk.Width; mHeight = (uint)mApng.IHDRChunk.Height; //生成Texture mTexture = new Texture2D(mApng.IHDRChunk.Width, mApng.IHDRChunk.Height); //生成ImagePixels mImagePixels = new ImagePixels(mWidth, mHeight); yield return null; mFrames.Clear(); int count = 0; for (int i = 0; i < mApng.Frames.Length; i++) { var frame = mApng.Frames[i]; //生成当前帧Texture var data = frame.GetStream().ToArray(); var texture = new Texture2D(1, 1); texture.LoadImage(data); var apngFrame = new APNGFrame(); apngFrame.index = i; apngFrame.frame = frame; //读取当前帧Texture像素数据 apngFrame.pixels = texture.GetPixels32(); //计算当前帧持续时间 apngFrame.duration = (float)frame.fcTLChunk.DelayNum / (float)frame.fcTLChunk.DelayDen; apngFrame.disposeOp = frame.fcTLChunk.DisposeOp; apngFrame.blendOp = frame.fcTLChunk.BlendOp; apngFrame.width = frame.fcTLChunk.Width; apngFrame.height = frame.fcTLChunk.Height; apngFrame.xOffset = frame.fcTLChunk.XOffset; //计算yOffset,因Texture采用的是从下到上顺序,所以这里需要翻转yOffset apngFrame.yOffset = mHeight - (frame.fcTLChunk.YOffset + apngFrame.height); mFrames.Add(apngFrame); Destroy(texture); count++; //每处理10帧,执行一下yield return以避免线程阻塞 if (count % 10 == 0) yield return null; } //yield return null; mLoadState = LoadState.READY; Debug.Log("This player is ready."); onReady?.Invoke(this); //autoPlay为true,直接开始播放 if (autoPlay) { Play(); } //为false,设置当前动画为第一帧 else { setCurrentFrameImpl(0); } } } /// /// 清除已加载的数据 /// public void Clear() { if (!isReady) { Debug.LogWarning("This player is not ready, can't call Clear() method."); return; } mPlayState = PlayState.STOPED; mLoadState = LoadState.UNLOADED; mCurrentFrameIndex = -1; mFrames.Clear(); mApng = null; if (mTexture != null) { Destroy(mTexture); mTexture = null; } } /// /// 开始播放 /// public void Play() { if (!isReady) { Debug.LogWarning("This player is not ready, can't call Play() method."); return; } //if (materials.Count == 0 && rawImages.Count == 0) //{ // Debug.LogWarning("The materials and rawImages count is 0, can't call Play() method."); // return; //} if (isPlaying) return; if (mCurrentFrameIndex == -1) setCurrentFrameImpl(0); mLastTime = Time.time; mPlayState = PlayState.PLAYING; } /// /// 停止播放 /// public void Stop() { if (!isReady) { Debug.LogWarning("This player is not ready, can't call Stop() method."); return; } if (isStoped) return; mPlayState = PlayState.STOPED; //恢复至第一帧 setCurrentFrameImpl(0); //重置动画循环次数 mLoopCount = 0; } /// /// 暂停播放 /// public void Pause() { if (!isReady) { Debug.LogWarning("This player is not ready, can't call Pause() method."); return; } if (isStoped || isPaused) return; mPlayState = PlayState.PAUSED; } /// /// 重新开始播放 /// public void Restart() { Stop(); Start(); } //public void SetCurrentFrame(int index) //{ // if (!isReady) // { // Debug.LogWarning("This player is not ready, can't call SetCurrentFrame() method."); // return; // } // if (material == null && rawImage == null) // { // Debug.LogWarning("The material and rawImage is null, can't call SetCurrentFrame() method."); // return; // } // if (index < 0 || index >= this.framesNumber) // { // Debug.LogWarning("SetCurrentFrame error, index " + index + " is out of bounds [" + 0 + ", " + this.framesNumber + ")"); // return; // } // setCurrentFrameImpl(index); //} //设置当前帧 private void setCurrentFrameImpl(int index) { if (mCurrentFrameIndex == index) return; mCurrentFrameIndex = index; var frame = mFrames[index]; if (textureMode == TextureMode.SingleTexture || (textureMode == TextureMode.MultiTexture && frame.texture == null)) { //第一帧 if (index == 0) { //绘制第一帧前将动画整体区域清空 mImagePixels.Clear(); //置空上一帧 mPrevFrame = null; } //存在上一帧 if (mPrevFrame != null) { switch (mPrevFrame.disposeOp) { case DisposeOps.APNGDisposeOpNone://不作处理,直接绘制 break; case DisposeOps.APNGDisposeOpBackground://清空上一帧区域 mImagePixels.ClearRect(mPrevFrame.xOffset, mPrevFrame.yOffset, mPrevFrame.width, mPrevFrame.height); break; case DisposeOps.APNGDisposeOpPrevious://恢复为上一帧绘制前的数据 mImagePixels.SetPixels(mPrevFrame.pixels, mPrevFrame.xOffset, mPrevFrame.yOffset, mPrevFrame.width, mPrevFrame.height); break; } } mPrevFrame = frame.Clone(); //存储当前的绘制数据,用于下一帧绘制前恢复该数据 if (mPrevFrame.disposeOp == DisposeOps.APNGDisposeOpPrevious) mPrevFrame.pixels = mImagePixels.GetPixels(mPrevFrame.xOffset, mPrevFrame.yOffset, mPrevFrame.width, mPrevFrame.height); //清空当前帧区域的数据 if (mPrevFrame.blendOp == BlendOps.APNGBlendOpSource) mImagePixels.ClearRect(mPrevFrame.xOffset, mPrevFrame.yOffset, mPrevFrame.width, mPrevFrame.height); //绘制当前帧 mImagePixels.SetPixels(frame.pixels, mPrevFrame.xOffset, mPrevFrame.yOffset, mPrevFrame.width, mPrevFrame.height); if (textureMode == TextureMode.SingleTexture) { if (frame.texture == null) frame.texture = mTexture; } else if (textureMode == TextureMode.MultiTexture) { if (frame.texture == null) frame.texture = new Texture2D(mApng.IHDRChunk.Width, mApng.IHDRChunk.Height); mTexture = frame.texture; } //将绘制好的数据设置给Texture mTexture.SetPixels32(mImagePixels.pixels); mTexture.Apply(); } else { mTexture = frame.texture; } //为Material与RawImage赋值 foreach (var material in materials) { material.mainTexture = mTexture; } foreach (var rawImage in rawImages) { rawImage.texture = mTexture; } onChanged?.Invoke(this, index, frame); } //获取下一帧索引 private int getNextFrameIndex() { var index = mCurrentFrameIndex; index++; //播放至最后一帧 if (index >= framesNumber) { index = 0; mLoopCount++; } return index; } //检查是否跳转下一帧 private void checkNextFrame() { //设置了最大循环次数且已循环次数超过最大次数,停止播放 if (maxLoopCount > 0 && mLoopCount >= maxLoopCount) { Stop(); return; } var nowTime = Time.time; if (nowTime - mLastTime >= mFrames[mCurrentFrameIndex].duration / playSpeed) { setCurrentFrameImpl(getNextFrameIndex()); mLastTime = nowTime; } } }