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.");
        }
    }

到目前为止,一切顺利。如果碰巧有问题,我会更新这个答案。