OpenGL ES 2.0 sprite batcher 正在为所有批次绘制一个纹理
OpenGL ES 2.0 sprite batcher is drawing one texture for all batches
我一直致力于将一些现有的游戏框架技术移植到 Android,因此,我一直在使用 OpenGL ES 2.0。我设置了一个与 XNA 的 SpriteBatch 非常相似的精灵批处理 class。虽然 class 在与 XNA 一起使用时可以正常工作,但在与我的 OpenGL 框架一起使用时会产生奇怪的结果。简而言之,它在正确的位置和正确的旋转方向绘制精灵,但最后绘制的纹理将用于每个精灵。
简单地说,如果我指示它绘制两个纹理 A 实例,然后绘制一个纹理 B 实例,那么它会绘制三个纹理 B 实例。如果我颠倒顺序,它会绘制三个纹理 A 实例. 最后发送的纹理将替换其他纹理。显而易见的答案是我正在替换所需插槽中的纹理。但是,我不相信我正在这样做(尽管我对 OpenGL 的了解有限)。为了帮助诊断问题,我在代码库中分散了控制台输出语句,只要调用了 OpenGL API(不包括着色器编译、链接等)。在 运行 代码到第一帧之后的断点之后,输出似乎表明在每个点都分配了正确的纹理。
GL: Setting front-face direction to CCW.
GL: Generating texture 1.
GL: Binding texture 1 to Texture2D.
GL: Setting texture data for Texture2D.
GL: Generating mipmap for Texture2D.
GL: Generating texture 2.
GL: Binding texture 2 to Texture2D.
GL: Setting texture data for Texture2D.
GL: Generating mipmap for Texture2D.
GL: Generating texture 3.
GL: Binding texture 3 to Texture2D.
GL: Setting texture data for Texture2D.
GL: Generating mipmap for Texture2D.
GL: Setting Viewport to 0,0,720,1134.
GL: Setting clear colour to: 0,0,0,0.
GL: Clearing Color Buffer.
GL: Setting blend equation separate to FuncAdd, FuncAdd.
GL: Setting blend function separate to One, OneMinusSrcAlpha, One, OneMinusSrcAlpha.
GL: Setting TextureMinFilter to 9987.
GL: Setting TextureMagFilter to 9729.
GL: Setting TextureWrapS to 33071.
GL: Setting TextureWrapT to 33071.
GL: Setting shader parameter 0 to Ozymandias.Mathematics.Matrix4. (MVP Matrix)
GL: Setting active texture unit to Texture0.
GL: Binding texture 2 to Texture2D.
GL: Setting shader parameter 1 to 0. (Texture Slot)
GL: Setting vertex attribute pointer for attribute 1. Current Texture ID: 2.
GL: Enabling vertex attribute array for attribute 1.
GL: Setting vertex attribute pointer for attribute 0. Current Texture ID: 2.
GL: Enabling vertex attribute array for attribute 0.
GL: Setting vertex attribute pointer for attribute 2. Current Texture ID: 2.
GL: Enabling vertex attribute array for attribute 2.
GL: Drawing elements. Current Texture ID: 2.
GL: Setting active texture unit to Texture0.
GL: Binding texture 1 to Texture2D.
GL: Setting shader parameter 1 to 0.
GL: Setting vertex attribute pointer for attribute 1. Current Texture ID: 1.
GL: Enabling vertex attribute array for attribute 1.
GL: Setting vertex attribute pointer for attribute 0. Current Texture ID: 1.
GL: Enabling vertex attribute array for attribute 0.
GL: Setting vertex attribute pointer for attribute 2. Current Texture ID: 1.
GL: Enabling vertex attribute array for attribute 2.
GL: Drawing elements. Current Texture ID: 1.
GL: Swapping Buffers.
日志显然表明在每次绘制和属性调用时都分配了正确的纹理。不幸的是,这个过程涉及到相当多的代码。纹理、图形和精灵批处理系统显然处于中心位置,但它也切线地触及效果系统。我会尽量避免发布不必要的代码,而不删除任何可能证明重要的内容。
精灵渲染器在刷新前运行设置。效果上不存在的任何参数都将被忽略。
private void Setup()
{
graphicsDevice.BlendState = blendState;
graphicsDevice.SamplerState = samplerState;
if (!effect.ApplyAfterParamters)
effect.Apply(0); // Apply first pass if effect applied before parameters set.
Matrix4 mvpMatrix = (Matrix4)(GraphicsDevice.Viewport.GetViewMatrix() * transformMatrix);
effect.SetParameter("MVPMatrix", mvpMatrix);
effect.SetParameter("TextureSlot", 0);
if (effect.ApplyAfterParamters)
effect.Apply(0); // Apply first pass if effect applied after parameters set.
}
然后它遍历精灵并绘制每批。值得注意的是,即使每个 sprite 都被视为自己的批次,也会出现此问题。
private void Flush()
{
if (sprites.Count == 0)
return;
int batchStart = 0;
Texture batchTexture = sprites[0].Texture;
for (int i = 1; i < sprites.Count; i++)
{
Texture currentTexture = sprites[i].Texture;
if (currentTexture.SortingID != batchTexture.SortingID)
{
if (i > batchStart)
RenderBatch(batchTexture, batchStart, i - batchStart);
batchTexture = currentTexture;
batchStart = i;
}
}
RenderBatch(batchTexture, batchStart, sprites.Count - batchStart);
sprites.Clear();
}
private void RenderBatch(Texture texture, int first, int count)
{
// Oversized batches and buffer capacity handled here.
// Set texture.
texture.SetActive(0); // Sets active slot to slot 0 and binds the texture.
effect.SetParameter("SpriteTexture", texture);
effect.SetParameter("Texture", texture);
if (effect.ApplyAfterParamters)
effect.Apply(0);
// Buffer position for draw offset.
int drawOffset = bufferPosition;
// Render sprites.
for (int i = 0; i < count; i++)
RenderSprite(sprites[first + i]);
int drawCount = bufferPosition - drawOffset;
// Draw primitives.
graphicsDevice.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, vertices, drawOffset * VerticesPerSprite, drawCount * VerticesPerSprite, indices, 0, drawCount * 2);
}
// RenderSprite populates the vertex array.
这是激活纹理以及设置效果纹理参数的方式。
// Effect setting texture parameter.
protected override void SetParameterImpl(int index, Graphics.Texture value)
{
GL.Uniform1(index, value.SlotIndex);
}
// Texture.SetActive
public override void SetActive(int index)
{
// Index checking here.
GL.ActiveTexture((TextureUnit)((int)TextureUnit.Texture0 + index));
GL.BindTexture(TextureTarget.Texture2D, this.TextureID);
GLHelper.CheckError();
this.index = index;
}
最后,这就是 OpenGL ES 2.0 图形设备处理图元绘制的方式。
protected override void DrawUserIndexedPrimitivesImpl<T>(PrimitiveType primitiveType, T[] vertices, int vertexOffset, int numVertices, short[] indices, int indexOffset, int primitiveCount, VertexDeclaration declaration)
{
// Validation here.
var vHandle = GCHandle.Alloc(vertices, GCHandleType.Pinned);
var iHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
ShaderVertexMap binding = currentProgram.GetBindings(declaration); // Maps vertex struct to shader program attributes.
ShaderAttribute attribute;
for (int i = 0; i < binding.Length; i++)
{
if (binding[i].Index >= 0)
{
attribute = currentProgram.Attributes[binding[i].Index];
GL.VertexAttribPointer(attribute.Index, binding[i].Size, binding[i].Type, binding[i].Normalised, declaration.Stride, IntPtr.Add(vHandle.AddrOfPinnedObject(), declaration.Elements[i].Offset));
GL.EnableVertexAttribArray(attribute.Index);
}
}
GLHelper.CheckError();
uint[] cIndices = new uint[indices.Length];
for (int i = 0; i < indices.Length; i++)
cIndices[i] = (uint)indices[i];
GL.DrawElements(GetMode(primitiveType), indices.Length - indexOffset, DrawElementsType.UnsignedShort, iHandle.AddrOfPinnedObject());
GLHelper.CheckError();
vHandle.Free();
iHandle.Free();
}
对于如此庞大的代码,我深表歉意,我已尽力将其缩减到看起来最相关的程度。显然在纹理 classes、效果、混合状态等方面有大量相关代码。但是,我觉得它们不太可能涉及,但我很乐意在必要时包含它们。我很难过,我真的很感激更有经验的人能够提供任何见解。我怀疑我在某处遗漏了一些非常基本的东西。
附录 1
我原本打算包括这个,但它让我忘记了。我在测试期间使用了许多简单的着色器,但我最常用的着色器是 OpenGL ES 2.0 编程指南中的一个示例的略微修改版本。顶点着色器:
uniform mat4 MVPMatrix;
attribute vec4 Position;
attribute vec4 Colour;
attribute vec2 TextureCoordinate;
varying vec4 vColour;
varying vec2 vTextureCoordinate;
void main()
{
vColour = Colour;
gl_Position = MVPMatrix * Position;
}
片段着色器:
precision mediump float;
uniform sampler2D Texture;
varying vec4 vColour;
varying vec2 vTextureCoordinate;
void main()
{
gl_FragColor = texture2D(Texture, vTextureCoordinate) * vColour;
}
*注意:这些是我手写的,所以可能有一些小错别字。
GraphicsDevice.DrawUserIndexedPrimitivesImpl<T>
方法中存在问题。此处介绍的方法不考虑索引或顶点偏移。它还错误地确定了要绘制的元素的数量。因此,它会为每个绘制调用绘制索引数组(~12000 个索引)。未启用混合,因此黑色背景上的透明纹理并不清楚。
已通过正确计算索引数组的偏移地址和长度解决问题。
protected override void DrawUserIndexedPrimitivesImpl<T>(PrimitiveType primitiveType, T[] vertices, int vertexOffset, int numVertices, short[] indices, int indexOffset, int primitiveCount, VertexDeclaration declaration)
{
if (vertices.Length == 0 || primitiveCount == 0)
return;
//source: https://github.com/mono/MonoGame/blob/develop/MonoGame.Framework/Graphics/GraphicsDevice.OpenGL.cs
GCHandle vHandle = GCHandle.Alloc(vertices, GCHandleType.Pinned);
GCHandle iHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
IntPtr vertexPointer = IntPtr.Add(vHandle.AddrOfPinnedObject(), declaration.Stride * vertexOffset);
IntPtr indexPointer = IntPtr.Add(iHandle.AddrOfPinnedObject(), sizeof(short) * indexOffset);
ShaderVertexAttributeMap map = currentProgram.GetMap(declaration);
for (int i = 0; i < map.Length; i++)
{
if (map[i].Index > -1)
{
GL.VertexAttribPointer(map[i].Index, map[i].Size, map[i].Type, map[i].Normalised, declaration.Stride, IntPtr.Add(vertexPointer, declaration.Elements[i].Offset));
GL.EnableVertexAttribArray(map[i].Index);
}
}
GLHelper.CheckError();
GL.DrawElements(GLConverter.Convert(primitiveType), IndexCount(primitiveType, primitiveCount), DrawElementsType.UnsignedShort, indexPointer);
GLHelper.CheckError();
vHandle.Free();
iHandle.Free();
}
private int IndexCount(PrimitiveType type, int count)
{
switch (type)
{
case PrimitiveType.LineList: return 2 * count;
case PrimitiveType.LineStrip: return count + 1;
case PrimitiveType.TriangleList: return 3 * count;
case PrimitiveType.TriangleStrip: return count + 2;
default: throw new ArgumentException("Unrecognised primitive type.");
}
}
到目前为止,一切顺利。如果碰巧有问题,我会更新这个答案。
我一直致力于将一些现有的游戏框架技术移植到 Android,因此,我一直在使用 OpenGL ES 2.0。我设置了一个与 XNA 的 SpriteBatch 非常相似的精灵批处理 class。虽然 class 在与 XNA 一起使用时可以正常工作,但在与我的 OpenGL 框架一起使用时会产生奇怪的结果。简而言之,它在正确的位置和正确的旋转方向绘制精灵,但最后绘制的纹理将用于每个精灵。
简单地说,如果我指示它绘制两个纹理 A 实例,然后绘制一个纹理 B 实例,那么它会绘制三个纹理 B 实例。如果我颠倒顺序,它会绘制三个纹理 A 实例. 最后发送的纹理将替换其他纹理。显而易见的答案是我正在替换所需插槽中的纹理。但是,我不相信我正在这样做(尽管我对 OpenGL 的了解有限)。为了帮助诊断问题,我在代码库中分散了控制台输出语句,只要调用了 OpenGL API(不包括着色器编译、链接等)。在 运行 代码到第一帧之后的断点之后,输出似乎表明在每个点都分配了正确的纹理。
GL: Setting front-face direction to CCW.
GL: Generating texture 1.
GL: Binding texture 1 to Texture2D.
GL: Setting texture data for Texture2D.
GL: Generating mipmap for Texture2D.
GL: Generating texture 2.
GL: Binding texture 2 to Texture2D.
GL: Setting texture data for Texture2D.
GL: Generating mipmap for Texture2D.
GL: Generating texture 3.
GL: Binding texture 3 to Texture2D.
GL: Setting texture data for Texture2D.
GL: Generating mipmap for Texture2D.
GL: Setting Viewport to 0,0,720,1134.
GL: Setting clear colour to: 0,0,0,0.
GL: Clearing Color Buffer.
GL: Setting blend equation separate to FuncAdd, FuncAdd.
GL: Setting blend function separate to One, OneMinusSrcAlpha, One, OneMinusSrcAlpha.
GL: Setting TextureMinFilter to 9987.
GL: Setting TextureMagFilter to 9729.
GL: Setting TextureWrapS to 33071.
GL: Setting TextureWrapT to 33071.
GL: Setting shader parameter 0 to Ozymandias.Mathematics.Matrix4. (MVP Matrix)
GL: Setting active texture unit to Texture0.
GL: Binding texture 2 to Texture2D.
GL: Setting shader parameter 1 to 0. (Texture Slot)
GL: Setting vertex attribute pointer for attribute 1. Current Texture ID: 2.
GL: Enabling vertex attribute array for attribute 1.
GL: Setting vertex attribute pointer for attribute 0. Current Texture ID: 2.
GL: Enabling vertex attribute array for attribute 0.
GL: Setting vertex attribute pointer for attribute 2. Current Texture ID: 2.
GL: Enabling vertex attribute array for attribute 2.
GL: Drawing elements. Current Texture ID: 2.
GL: Setting active texture unit to Texture0.
GL: Binding texture 1 to Texture2D.
GL: Setting shader parameter 1 to 0.
GL: Setting vertex attribute pointer for attribute 1. Current Texture ID: 1.
GL: Enabling vertex attribute array for attribute 1.
GL: Setting vertex attribute pointer for attribute 0. Current Texture ID: 1.
GL: Enabling vertex attribute array for attribute 0.
GL: Setting vertex attribute pointer for attribute 2. Current Texture ID: 1.
GL: Enabling vertex attribute array for attribute 2.
GL: Drawing elements. Current Texture ID: 1.
GL: Swapping Buffers.
日志显然表明在每次绘制和属性调用时都分配了正确的纹理。不幸的是,这个过程涉及到相当多的代码。纹理、图形和精灵批处理系统显然处于中心位置,但它也切线地触及效果系统。我会尽量避免发布不必要的代码,而不删除任何可能证明重要的内容。
精灵渲染器在刷新前运行设置。效果上不存在的任何参数都将被忽略。
private void Setup()
{
graphicsDevice.BlendState = blendState;
graphicsDevice.SamplerState = samplerState;
if (!effect.ApplyAfterParamters)
effect.Apply(0); // Apply first pass if effect applied before parameters set.
Matrix4 mvpMatrix = (Matrix4)(GraphicsDevice.Viewport.GetViewMatrix() * transformMatrix);
effect.SetParameter("MVPMatrix", mvpMatrix);
effect.SetParameter("TextureSlot", 0);
if (effect.ApplyAfterParamters)
effect.Apply(0); // Apply first pass if effect applied after parameters set.
}
然后它遍历精灵并绘制每批。值得注意的是,即使每个 sprite 都被视为自己的批次,也会出现此问题。
private void Flush()
{
if (sprites.Count == 0)
return;
int batchStart = 0;
Texture batchTexture = sprites[0].Texture;
for (int i = 1; i < sprites.Count; i++)
{
Texture currentTexture = sprites[i].Texture;
if (currentTexture.SortingID != batchTexture.SortingID)
{
if (i > batchStart)
RenderBatch(batchTexture, batchStart, i - batchStart);
batchTexture = currentTexture;
batchStart = i;
}
}
RenderBatch(batchTexture, batchStart, sprites.Count - batchStart);
sprites.Clear();
}
private void RenderBatch(Texture texture, int first, int count)
{
// Oversized batches and buffer capacity handled here.
// Set texture.
texture.SetActive(0); // Sets active slot to slot 0 and binds the texture.
effect.SetParameter("SpriteTexture", texture);
effect.SetParameter("Texture", texture);
if (effect.ApplyAfterParamters)
effect.Apply(0);
// Buffer position for draw offset.
int drawOffset = bufferPosition;
// Render sprites.
for (int i = 0; i < count; i++)
RenderSprite(sprites[first + i]);
int drawCount = bufferPosition - drawOffset;
// Draw primitives.
graphicsDevice.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, vertices, drawOffset * VerticesPerSprite, drawCount * VerticesPerSprite, indices, 0, drawCount * 2);
}
// RenderSprite populates the vertex array.
这是激活纹理以及设置效果纹理参数的方式。
// Effect setting texture parameter.
protected override void SetParameterImpl(int index, Graphics.Texture value)
{
GL.Uniform1(index, value.SlotIndex);
}
// Texture.SetActive
public override void SetActive(int index)
{
// Index checking here.
GL.ActiveTexture((TextureUnit)((int)TextureUnit.Texture0 + index));
GL.BindTexture(TextureTarget.Texture2D, this.TextureID);
GLHelper.CheckError();
this.index = index;
}
最后,这就是 OpenGL ES 2.0 图形设备处理图元绘制的方式。
protected override void DrawUserIndexedPrimitivesImpl<T>(PrimitiveType primitiveType, T[] vertices, int vertexOffset, int numVertices, short[] indices, int indexOffset, int primitiveCount, VertexDeclaration declaration)
{
// Validation here.
var vHandle = GCHandle.Alloc(vertices, GCHandleType.Pinned);
var iHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
ShaderVertexMap binding = currentProgram.GetBindings(declaration); // Maps vertex struct to shader program attributes.
ShaderAttribute attribute;
for (int i = 0; i < binding.Length; i++)
{
if (binding[i].Index >= 0)
{
attribute = currentProgram.Attributes[binding[i].Index];
GL.VertexAttribPointer(attribute.Index, binding[i].Size, binding[i].Type, binding[i].Normalised, declaration.Stride, IntPtr.Add(vHandle.AddrOfPinnedObject(), declaration.Elements[i].Offset));
GL.EnableVertexAttribArray(attribute.Index);
}
}
GLHelper.CheckError();
uint[] cIndices = new uint[indices.Length];
for (int i = 0; i < indices.Length; i++)
cIndices[i] = (uint)indices[i];
GL.DrawElements(GetMode(primitiveType), indices.Length - indexOffset, DrawElementsType.UnsignedShort, iHandle.AddrOfPinnedObject());
GLHelper.CheckError();
vHandle.Free();
iHandle.Free();
}
对于如此庞大的代码,我深表歉意,我已尽力将其缩减到看起来最相关的程度。显然在纹理 classes、效果、混合状态等方面有大量相关代码。但是,我觉得它们不太可能涉及,但我很乐意在必要时包含它们。我很难过,我真的很感激更有经验的人能够提供任何见解。我怀疑我在某处遗漏了一些非常基本的东西。
附录 1 我原本打算包括这个,但它让我忘记了。我在测试期间使用了许多简单的着色器,但我最常用的着色器是 OpenGL ES 2.0 编程指南中的一个示例的略微修改版本。顶点着色器:
uniform mat4 MVPMatrix;
attribute vec4 Position;
attribute vec4 Colour;
attribute vec2 TextureCoordinate;
varying vec4 vColour;
varying vec2 vTextureCoordinate;
void main()
{
vColour = Colour;
gl_Position = MVPMatrix * Position;
}
片段着色器:
precision mediump float;
uniform sampler2D Texture;
varying vec4 vColour;
varying vec2 vTextureCoordinate;
void main()
{
gl_FragColor = texture2D(Texture, vTextureCoordinate) * vColour;
}
*注意:这些是我手写的,所以可能有一些小错别字。
GraphicsDevice.DrawUserIndexedPrimitivesImpl<T>
方法中存在问题。此处介绍的方法不考虑索引或顶点偏移。它还错误地确定了要绘制的元素的数量。因此,它会为每个绘制调用绘制索引数组(~12000 个索引)。未启用混合,因此黑色背景上的透明纹理并不清楚。
已通过正确计算索引数组的偏移地址和长度解决问题。
protected override void DrawUserIndexedPrimitivesImpl<T>(PrimitiveType primitiveType, T[] vertices, int vertexOffset, int numVertices, short[] indices, int indexOffset, int primitiveCount, VertexDeclaration declaration)
{
if (vertices.Length == 0 || primitiveCount == 0)
return;
//source: https://github.com/mono/MonoGame/blob/develop/MonoGame.Framework/Graphics/GraphicsDevice.OpenGL.cs
GCHandle vHandle = GCHandle.Alloc(vertices, GCHandleType.Pinned);
GCHandle iHandle = GCHandle.Alloc(indices, GCHandleType.Pinned);
IntPtr vertexPointer = IntPtr.Add(vHandle.AddrOfPinnedObject(), declaration.Stride * vertexOffset);
IntPtr indexPointer = IntPtr.Add(iHandle.AddrOfPinnedObject(), sizeof(short) * indexOffset);
ShaderVertexAttributeMap map = currentProgram.GetMap(declaration);
for (int i = 0; i < map.Length; i++)
{
if (map[i].Index > -1)
{
GL.VertexAttribPointer(map[i].Index, map[i].Size, map[i].Type, map[i].Normalised, declaration.Stride, IntPtr.Add(vertexPointer, declaration.Elements[i].Offset));
GL.EnableVertexAttribArray(map[i].Index);
}
}
GLHelper.CheckError();
GL.DrawElements(GLConverter.Convert(primitiveType), IndexCount(primitiveType, primitiveCount), DrawElementsType.UnsignedShort, indexPointer);
GLHelper.CheckError();
vHandle.Free();
iHandle.Free();
}
private int IndexCount(PrimitiveType type, int count)
{
switch (type)
{
case PrimitiveType.LineList: return 2 * count;
case PrimitiveType.LineStrip: return count + 1;
case PrimitiveType.TriangleList: return 3 * count;
case PrimitiveType.TriangleStrip: return count + 2;
default: throw new ArgumentException("Unrecognised primitive type.");
}
}
到目前为止,一切顺利。如果碰巧有问题,我会更新这个答案。