1. 动画精灵概念
动画就是动态的画面,在计算机中表现为一种运行时数据结构和算法。数据结构表示动画的存储方式,可以事先存储,也可以运行时计算获得。
而算法则声明如何将这种数据结构映射到屏幕上。
精灵是一个渲染单元,存储一些表示如何渲染到屏幕上的数据。
动画精灵是一种特殊的精灵,因此这次的目标就是创建一个精灵类(Sprite)的子类(AnimatedSprite).
2. Sprite类
在设计之前,需要预览一下Sprite类的代码。(省略了用于缩放和旋转的部分代码)
/// <summary>
/// 精灵类
/// </summary>
public class Sprite
{
/// <summary>
/// 顶点数量
/// </summary>
const int VertexAmount = 6; //2个三角形
/// <summary>
/// 顶点坐标
/// </summary>
Vector[] _vertexPositions = new Vector[VertexAmount];
/// <summary>
/// 顶点色彩(局部色彩)
/// </summary>
GlColor[] _vertexColors = new GlColor[VertexAmount];
/// <summary>
/// 顶点映射(纹理贴图用于渲染多边形的特定部分)
/// </summary>
GlPoint[] _vertexUVs = new GlPoint[VertexAmount];
/// <summary>
/// 纹理
/// </summary>
Texture2D _texture = new Texture2D();
/// <summary>
/// 获取或设置精灵纹理
/// </summary>
public Texture2D Texture
{
get { return _texture; }
set
{
_texture = value;
//默认使用纹理自身的宽高
InitVertexPositions(CenterPosition, _texture.Width, _texture.Height);
}
}
/// <summary>
/// 获取顶点数组
/// </summary>
public Vector[] VertexPositions
{
get { return _vertexPositions; }
}
/// <summary>
/// 获取顶点颜色数组
/// </summary>
public GlColor[] VertexColors
{
get { return _vertexColors; }
}
/// <summary>
/// 获取顶点坐标数组
/// </summary>
public GlPoint[] VertexUVs
{
get { return _vertexUVs; }
}
/// <summary>
/// 获取或设置宽度
/// </summary>
public double Width
{
get
{
// 获取实际显示在屏幕上的宽度
return _vertexPositions[1].X - _vertexPositions[0].X;
}
set
{
InitVertexPositions(CenterPosition, value, Height);
}
}
/// <summary>
/// 获取或设置高度
/// </summary>
public double Height
{
get
{
// topleft - bottomleft
return _vertexPositions[0].Y - _vertexPositions[2].Y;
}
set
{
InitVertexPositions(CenterPosition, Width, value);
}
}
/// <summary>
/// 创建一个精灵
/// </summary>
public Sprite()
{
InitVertexPositions(Vector.Zero, 1, 1);
SetColor(GlColor.GetWhite());
SetUVs(new GlPoint(0, 0), new GlPoint(1, 1));
//正确设置默认的初始位置
_currentPosition = new Vector(
_vertexPositions[0].X + Width / 2,
_vertexPositions[0].Y - Height / 2,
_vertexPositions[0].Z);
}
/// <summary>
/// 初始化顶点信息
/// </summary>
void InitVertexPositions(Vector center, double width, double height)
{
double halfWidth = width / 2;
double halfHeight = height / 2;
//顺时针创建两个三角形构成四方形
// TopLeft, TopRight, BottomLeft
_vertexPositions[0] = new Vector(center.X - halfWidth, center.Y + halfHeight, center.Z);
_vertexPositions[1] = new Vector(center.X + halfWidth, center.Y + halfHeight, center.Z);
_vertexPositions[2] = new Vector(center.X - halfWidth, center.Y - halfHeight, center.Z);
// TopRight, BottomRight, BottomLeft
_vertexPositions[3] = new Vector(center.X + halfWidth, center.Y + halfHeight, center.Z);
_vertexPositions[4] = new Vector(center.X + halfWidth, center.Y - halfHeight, center.Z);
_vertexPositions[5] = new Vector(center.X - halfWidth, center.Y - halfHeight, center.Z);
}
/// <summary>
/// 获取或设置中心位置
/// </summary>
public Vector CenterPosition
{
get
{
return _currentPosition;
}
set
{
Matrix m = new Matrix();
m.SetTranslation(value - _currentPosition);
ApplyMatrix(m);
_currentPosition = value;
}
}
/// <summary>
/// 设置颜色
/// </summary>
public void SetColor(GlColor color)
{
for (int i = 0; i < Sprite.VertexAmount; i++)
{
_vertexColors[i] = color;
}
}
/// <summary>
/// 设置UV,进行纹理映射
/// </summary>
public void SetUVs(GlPoint topLeft, GlPoint bottomRight)
{
// TopLeft, TopRight, BottomLeft
_vertexUVs[0] = topLeft;
_vertexUVs[1] = new GlPoint(bottomRight.X, topLeft.Y);
_vertexUVs[2] = new GlPoint(topLeft.X, bottomRight.Y);
// TopRight, BottomRight, BottomLeft
_vertexUVs[3] = new GlPoint(bottomRight.X, topLeft.Y);
_vertexUVs[4] = bottomRight;
_vertexUVs[5] = new GlPoint(topLeft.X, bottomRight.Y);
}
/// <summary>
/// 应用矩阵操作
/// </summary>
public void ApplyMatrix(Matrix m)
{
for (int i = 0; i < VertexPositions.Length; i++)
{
VertexPositions[i] *= m;
}
}
}
sprite类只存储用于绘制的数据,自己不负责绘制自身,而是交由Render类负责。
Matrix类用于矩阵运算,应用矩阵可以实现平移、缩放、旋转的效果,简化了sprite类的设计。
3. 一致的素材格式
在flash中,只有关键帧由用户提供,其余帧通过补间完成,而这次设计的动画精灵的所有帧全部由用户提供(来源于图片素材),这样大大简化了设计。
因此所面临的问题只是如何收集和呈现素材。“收集”即将零散的图片资源加载为有序组织的数据结构,“呈现”即将内存的数据利用图形渲染器绘制屏幕上。
我选择C#+opengl的渲染构件,已经实现将普通sprite渲染到屏幕上,只需要考虑如何依次的向opengl传送需要渲染的纹理,双缓冲和其他的渲染技术不需考虑。
动画所必须的素材可能拥有不同的呈现格式,但需要将这些素材加载为一致内存表示。大致有两种方式可以解决这个问题。
第一就是对动画素材进行预处理,通过非编程的手段将素材表示为一致的标准格式。
第二就是针对不同的格式制定相应的加载算法。很明显应该采用第一种解决方案,这有利于简化系统设计。
约定一下的素材格式:
1.一种动画精灵由一个图像文件(png/jpg/...)提供。
2.动画由若干帧组成,每帧的大小是相同的
3.帧按文字的排列方式排列,不能留空隙。
4.除了提供文件之外,还需指明帧的大小和数量。
4. 动画精灵的类设计
由于已经存在sprite类,所以可以简单的通过继承复用一些方法。我们需要做的就是将当前帧映射到显示数据上(顶点数组、颜色数组、UV数组),
这通过运行时计算获得,也可以事先计算。
默认的UV包含整个纹理图像,这就是常规的sprite显示方式-显示整个图像。可以通过修改UV只显示部分图像,
如果每一次当前帧改变的时候都重新设置UV以把纹理图像中代表相应帧的画面显示出来,就可以实现动画的效果。
通过倒推法可以完成此项设计。
->知道当前帧,如何设置UV数据
->调用基类的SetUVs方法,传递两个能够唯一确定显示区域的坐标。
->已经知道整张图像的大小和单位帧的大小,只要知道当前帧在纹理图像上的左上角坐标就可以了。
->可以计算得知一行有多少帧,而且已经知道了帧的大小...
->OK
这其实并不是倒推法,这是解决问题的真正的正常思路。总不可能从条件出发,那才是违逆正常思维的倒推法。
/// <summary>
/// 动画精灵
/// </summary>
public class AnimatedSprite : Sprite
{
/// <summary>
/// 总帧数
/// </summary>
int _totalFrame;
/// <summary>
/// 帧宽
/// </summary>
double _frameWidth;
/// <summary>
/// 帧高
/// </summary>
double _frameHeight;
/// <summary>
/// 当前帧
/// </summary>
int _currentFrame;
/// <summary>
/// 当前帧的计时
/// </summary>
double _currentFrameTime;
/// <summary>
/// 获取或设置每一帧持续的时间
/// </summary>
public double FrameDuration { get; set; }
/// <summary>
/// 是否循环播放
/// </summary>
public bool Looping { get; set; }
/// <summary>
/// 是否播放结束
/// </summary>
public bool Finished { get; private set; }
/// <summary>
/// 获取一行的帧数量
/// </summary>
public int RowFrame
{
get
{
return (int)Math.Round(Width / _frameWidth);
}
}
/// <summary>
/// 创建一个可播放的动画精灵
/// </summary>
public AnimatedSprite()
{
Looping = false;
Finished = false;
FrameDuration = 0.05;
_frameHeight = 0;
_frameWidth = 0;
_currentFrame = 0;
_totalFrame = 1;
_currentFrameTime = FrameDuration;
}
/// <summary>
/// 拨动到下一帧
/// </summary>
public void AdvanceFrame()
{
_currentFrame = (_currentFrame + 1) % _totalFrame;
}
/// <summary>
/// 获取帧值在图源中的位置索引
/// </summary>
GlPoint GetIndexFromFrame(int frame)
{
GlPoint point = new GlPoint();
point.Y = frame / RowFrame;
point.X = frame - (point.Y * RowFrame);
return point;
}
/// <summary>
/// 更新显示数据
/// </summary>
void UpdateDisplay()
{
GlPoint index = GetIndexFromFrame(_currentFrame);
Vector startPosition = new Vector(index.X * _frameWidth, index.Y * _frameHeight);
Vector endPosition = startPosition + new Vector(_frameWidth, _frameHeight);
SetUVs(new GlPoint((float)(startPosition.X / Width), (float)(startPosition.Y / Height)),
new GlPoint((float)(endPosition.X / Width), (float)(endPosition.Y / Height)));
}
/// <summary>
/// 通知动画精灵的总帧数
/// </summary>
public void SetTotalFrame(int totalFrame)
{
_totalFrame = totalFrame;
_currentFrame = 0;
_currentFrameTime = FrameDuration;
UpdateDisplay();
}
/// <summary>
/// 通知动画精灵的帧大小,每一帧将应用一致的尺寸
/// </summary>
public void SetFrameSize(double width, double height)
{
_frameWidth = width;
_frameHeight = height;
UpdateDisplay();
}
/// <summary>
/// 处理更新
/// </summary>
public override void Process(double elapsedTime)
{
if (_currentFrame == _totalFrame - 1 && Looping == false)
{
Finished = true;
return;
}
_currentFrameTime -= elapsedTime;
if (_currentFrameTime < 0)
{
_currentFrameTime = FrameDuration;
AdvanceFrame();
UpdateDisplay();
}
}
/// <summary>
/// 缩放时维护帧大小
/// </summary>
public override void SetScale(double x, double y)
{
_frameWidth /= _scaleX;
_frameHeight /= _scaleY;
base.SetScale(x, y);
_frameWidth *= x;
_frameHeight *= y;
UpdateDisplay();
}
}
5. 测试
这里写了简单的测试代码,更重要的测试是在实际运行时观察效果。
public void ProcessTest()
{
AnimatedSprite target = new AnimatedSprite();
target.Texture = new Texture2D(0, 256, 256);
target.SetTotalFrame(16);
target.SetFrameSize(64, 64);
target.CenterPosition = Vector.Zero;
Assert.IsTrue(target.CurrentFrame == 0);
Assert.IsTrue(target.Finished == false);
double elapsedTime = 0.32;
MultiProcess(target, elapsedTime);
Assert.IsTrue(target.CurrentFrame == 3);
MultiProcess(target, elapsedTime);
MultiProcess(target, elapsedTime);
MultiProcess(target, elapsedTime);
MultiProcess(target, elapsedTime);
MultiProcess(target, elapsedTime);
MultiProcess(target, elapsedTime);
MultiProcess(target, elapsedTime);
Assert.IsTrue(target.Finished == true);
Assert.IsTrue(target.CurrentFrame == 15);
}