- Edited
Problem using an effect with multiple passes
I'm using MonoGame in C# and rendering billboarded Spine characters into a 3D world. Things are generally working, but I'm trying to do better handling of alpha and opaque objects throughout my code to alleviate some rendering bugs. To accomplish this, I'm trying to render my Spine characters using an effect with two passes, the first to render the opaque pixels and write to the Z buffer, and the second to draw transparent pixels and not update the Z buffer, but to do alpha blending.
If interested, I'm generally doing the same thing described here, albeit with newer versions of the code:
http://www.riemers.net/eng/Tutorials/XNA/Csharp/Series4/Billboarding_renderstates.php
The issue I'm running into is that my Spine skeletons don't seem to be rendering my second pass How I'm currently trying to handle this is as follows:
C# code Snippet:
Matrix billboardWorld = Matrix.Invert( camera.viewMatrix );
billboardWorld.Translation = _WorldPosition;
skelRender.Effect.Parameters["World"].SetValue( worldScale * billboardWorld );
skelRender.Effect.Parameters["Projection"].SetValue( camera.Projection );
skelRender.Effect.Parameters["View"].SetValue( camera.viewMatrix );
skelRender.Begin();
skelRender.Draw( skel );
skelRender.End();
HLSL Snippets:
float4 BSPS(VSOutput input) : SV_TARGET
{
return float4(1, 1, 1, 1);
}
technique CharacterLit
{
pass P0
{
VertexShader = compile vs_4_0 VSQuad();
PixelShader = compile ps_4_0 PSQuad();
}
pass P1
{
VertexShader = compile vs_4_0 VSBS();
PixelShader = compile ps_4_0 BSPS();
}
}
The problem lies in the fact that when calling the SkeletonRenderer Begin / Draw / End calls, when it finally goes to render things via the End call, it does this:
public void End () {
foreach (EffectPass pass in effect.CurrentTechnique.Passes) {
pass.Apply();
batcher.Draw(device);
}
}
public void Draw (GraphicsDevice device) {
if (items.Count == 0) return;
int itemCount = items.Count;
int vertexCount = 0, triangleCount = 0;
for (int i = 0; i < itemCount; i++) {
MeshItem item = items[i];
vertexCount += item.vertexCount;
triangleCount += item.triangleCount;
}
EnsureCapacity(vertexCount, triangleCount);
vertexCount = 0;
triangleCount = 0;
Texture2D lastTexture = null;
for (int i = 0; i < itemCount; i++) {
MeshItem item = items[i];
int itemVertexCount = item.vertexCount;
if (item.texture != lastTexture || vertexCount + itemVertexCount > short.MaxValue) {
FlushVertexArray(device, vertexCount, triangleCount);
vertexCount = 0;
triangleCount = 0;
lastTexture = item.texture;
device.Textures[0] = lastTexture;
}
int[] itemTriangles = item.triangles;
int itemTriangleCount = item.triangleCount;
for (int ii = 0, t = triangleCount; ii < itemTriangleCount; ii++, t++)
triangles[t] = (short)(itemTriangles[ii] + vertexCount);
triangleCount += itemTriangleCount;
Array.Copy(item.vertices, 0, vertexArray, vertexCount, itemVertexCount);
vertexCount += itemVertexCount;
item.texture = null;
freeItems.Enqueue(item);
}
FlushVertexArray(device, vertexCount, triangleCount);
items.Clear();
}
The issue is that after rendering the first pass, it calls items.Clear() at the end of the MeshBatcher draw call. When it comes in a second time for the second pass, it hits the early out because items.Count is 0.
I'm curious if this is a bug, or if I'm just not doing it in the intended way? I believe I could fix this by calling the SkeletonRenderer Begin / Draw / End calls inside a loop that goes through each pass, but then it makes the code in SkeletonRenderer.End that loops through the passes seem pointless.
Sorry for the trouble! Thanks for reporting, this indeed seems to be a bug in the code.
I have created an issue ticket here: https://github.com/EsotericSoftware/spine-runtimes/issues/1554.
A bugfix has just been committed, please let us know if everything is now fine on your side as well!
Thanks Harald! I pulled those changes over and it did indeed fix the problem. I am now seeing both passes applied when rendering.
I really appreciate how quickly you were able to take a look at and solve this!
Actually, I may have spoken too soon.
Your change does fix the original problem, but unfortunately it reveals a new one. On the second pass, we're not getting any texture information. Looks like this may be a result of the following:
for (int i = 0; i < itemCount; i++) {
MeshItem item = items[i];
int itemVertexCount = item.vertexCount;
if (item.texture != lastTexture || vertexCount + itemVertexCount > short.MaxValue) {
FlushVertexArray(device, vertexCount, triangleCount);
vertexCount = 0;
triangleCount = 0;
lastTexture = item.texture;
device.Textures[0] = lastTexture;
}
int[] itemTriangles = item.triangles;
int itemTriangleCount = item.triangleCount;
for (int ii = 0, t = triangleCount; ii < itemTriangleCount; ii++, t++)
triangles[t] = (short)(itemTriangles[ii] + vertexCount);
triangleCount += itemTriangleCount;
Array.Copy(item.vertices, 0, vertexArray, vertexCount, itemVertexCount);
vertexCount += itemVertexCount;
item.texture = null;
freeItems.Enqueue(item);
}
On the first pass through we're fine, but item.texture is getting set to null for all the MeshItem objects. So when we come back into the Draw function a second time, the if check that looks at lastTexture always fails because all the MeshItem objects have null set as their texture.
I think perhaps setting the textures to null should also be done in the new AfterLastDrawPass function? I did a quick test and the results are now what I expect. However I'm not sure if there might be any issues that could arise from changing this? Things seem to be working locally with this change, but I definitely don't have every use case covered. What do you think?
Thanks again very much for reporting! This was not immediately noticed since it worked with a single atlas texture.
This issue is now fixed as well, along with another bugfix: freeItems.Enqueue(item);
was called twice before - fixed this as well along the way.
Please let us know if it now covered your problems as well! Thanks again!
I do believe everything is working now. I pulled down your latest changes and have been running with them all day and everything is behaving as expected. Thanks so much for the help and for handling this so quickly!
Thanks for getting back to us, glad it works now!