我通常在XNA / Monogame中使用SpriteBatch
进行2D游戏,并且最近刚刚研究了诸如DrawUserIndexedPrimatives
之类的3D绘图方法。我正在开发一个项目,我们的动画师希望能够剪切精灵和纹理。
使用SpriteBatch
,您可以在SpriteBatch
上传入矩阵,开始剪切对象。类似的东西:
//translate object to origin
Matrix translate1 = Matrix.CreateTranslation(-rectangle.X, -rectangle.Y, 0);
//skew the sprite 33 degrees on the X and Y axis
Matrix skew = Matrix.Identity;
skew.M12 = (float)Math.Tan(33 * 0.0174532925f);
skew.M21 = (float)Math.Tan(33 * 0.0174532925f);
//translate object back
Matrix translate2 = Matrix.CreateTranslation(rectangle.X, rectangle.Y, 0);
Matrix transform = translate1 * skew * translate2;
_spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied,
SamplerState.PointWrap, DepthStencilState.Default,
RasterizerState.CullCounterClockwise, null, transform);
_spriteBatch.Draw(_texture, rectangle, Color.White);
_spriteBatch.End();
明显的缺点是它要求你为每个剪切的精灵进行一次新的SpriteBatch
开始和结束调用。我们目前在游戏中只需要拨打SpriteBatch
两个电话。一个用于UI,一个用于世界的东西。我们的艺术家想要使用剪切来做摇晃的树木或动物的腿和肢体的动物,所以如果我们给他们选择,我可以看到这个数字跳到10多个不同的批次。
平均水平有大约250个元素,每个元素包含10-20个精灵。
我已经为Android编写了一个测试,调用1000个精灵。没有任何偏斜,它可以在大约11秒或大约53fps中绘制所有1000,600次。但如果我倾斜每十个精灵(增加100个新的SpriteBatch
调用),则需要47秒,或大约12fps。
这真的很糟糕。即使只有200个精灵(每十分之一倾斜),测试也会下降到28fps。
所以我也使用用DrawUserIndexedPrimitives
绘制的四边形创建了相同的测试。每个Quad使用在Game类中创建的共享BasicEffect
,并通过Sprite
类构造函数传入。我在每个pass.Apply()
之前设置了World Matrix和Texture,如下所示:
if (_basicEffect != null)
{
foreach (EffectPass pass in _basicEffect.CurrentTechnique.Passes)
{
_basicEffect.World = Transform;
_basicEffect.Texture = _texture;
pass.Apply();
GraphicsDevice.DrawUserIndexedPrimitives
<VertexPositionNormalTexture>(
PrimitiveType.TriangleList,
_quad.Vertices, 0, 4,
_quad.Indices, 0, 2);
}
对于1000个精灵,没有歪斜,这给了我12fps(我想它就像进行1000次spriteBatch
调用)。那真的很糟糕。但是对于每10个精灵倾斜只有200个精灵,我得到46fps,明显优于SpriteBatch
(即使我正在调用DrawUserIndexedPrimitives
200次)。
---我的问题---
我如何批量调用DrawUserIndexedPrimitives
(或类似的东西),同时保持我的精灵每个都包含在继承DrawableGameComponent
的自己的类中?最后一部分非常重要,仅仅是因为我们的游戏引擎的性质以及处理动画和碰撞的方式。
我已经阅读了关于Vertex Buffers和DrawIndexedPrimitives
的内容,但是我的脑袋并没有完全缠绕它,也不知道我是如何为绘制的精灵分配新纹理和世界变换的就这样。
如果我批量调用这些电话,我是否应该期望与SpriteBatch
相似/更好的性能?
答案 0 :(得分:2)
在我看来,你有几个选择,在这里。请注意,我主要熟悉PC上的XNA 4.0,因此在您的情况下,并非所有这些都可能/高效。
简单,难懂的方式
绘制精灵时,您似乎没有使用颜色通道;这种技术假设您的示例代表您的真实代码。
如果你不需要精灵颜色来调整你的精灵,你可以劫持它作为将每个精灵数据传递到自定义顶点/像素着色器的方法。例如,您可以这样做:
var shearX = MathHelper.ToRadians(33) / MathHelper.TwoPi;
var shearY = MathHelper.ToRadians(33) / MathHelper.TwoPi;
var color = new Color(shearX, shearY, 0f, 0f);
_spriteBatch.Draw(_texture, rectangle, color);
这表示x和y剪切值分别作为存储在红色和绿色通道中的2 * pi
的因子。
然后,您可以创建一个自定义顶点着色器,用于检索这些值并动态执行剪切计算。有关如何执行此操作的信息,请参阅Shawn Hargreaves的文章here。
混合方法
另一个相对简单的可能性是将传统精灵批处理与您的DrawUserIndexedPrimitives
代码相结合。
良好性能的关键是最小化状态变化,因此仔细订购精灵可能会有很长的路要走。组织你的精灵,你可以使用SpriteBatch
一次性绘制所有非倾斜的精灵,然后只使用较慢的DrawUserIndexedPrimitives
技术来绘制实际需要它的精灵。假设给定帧中的大多数精灵没有倾斜,这应该会显着减少发送到GPU的批次数。
批处理+自定义顶点格式
这可能是最好的技术,但它也涉及编写大多数代码。并非任何一个都特别复杂。
SpriteBatch
内部工作的方式是它维护一个动态顶点缓冲区,该缓冲区填充在CPU上,然后在一次调用中全部绘制。 Shawn Hargreaves提供了关于如何完成此类事情的高级概述here。
扩展你的DrawUserIndexedPrimitives
以使用这种技术的问题是那个讨厌的世界矩阵;着色器实际上并没有很好的方法将特定的世界矩阵附加到特定的sprite(除非你使用硬件实例,我认为你的平台不支持)。那你能做什么?
如果创建自定义顶点格式,则可以将剪切值附加到每个顶点,并使用它们在顶点着色器中执行剪切,如第一种技术中所示。这将允许您在一次通话中绘制所有游戏的精灵,这应该非常快。
您可以找到有关自定义顶点声明here的信息。