2D Billboard sprites strange offset when rotating camera

Hey all, I’ve followed this thread Problem Solving: 2D Billboard Sprites clipping into 3D environment - #68 by claudio04 religiously for the past year or so while developing my game. It helped me fix the issues reported on the thread but now I’ve got another issue when rotating the camera and didn’t want to revive that old thread.

If the camera is “centered”/facing north, the sprites render normally

However as soon as I start rotating the camera I can see some displacement
image

Here’s a 360 video:

And this is how I have my shader

float rayPlaneIntersection(float3 rayDir, float3 rayPos, float3 planeNormal, float3 planePos)
{
    float denom = dot(planeNormal, rayDir);
    denom = max(denom, 0.000001);
    float3 diff = planePos - rayPos;
    return dot(diff, planeNormal) / denom;
}

float4 billboardMeshTowardsCamera(float3 vertex, float4 offset, float4 uv)
{
    // billboard mesh towards camera
    float3 vpos = mul((float3x3)unity_ObjectToWorld, vertex.xyz);
    float4 worldCoord = float4(unity_ObjectToWorld._m03_m13_m23, 1);
    float4 viewPivot = mul(UNITY_MATRIX_V, worldCoord);

    // construct rotation matrix
    float3 forward = -normalize(viewPivot);
    float3 up = mul(UNITY_MATRIX_V, float3(0,1,0)).xyz;
    float3 right = normalize(cross(up,forward));
    up = cross(forward,right);
    float3x3 facingRotation = float3x3(right, up, forward);

    float4 viewPos = float4(viewPivot + mul(vpos, facingRotation), 1.0);
    float4 pos = mul(UNITY_MATRIX_P, viewPos + offset);

    // calculate distance to vertical billboard plane seen at this vertex's screen position
    const float3 planeNormal = normalize((_WorldSpaceCameraPos.xyz - unity_ObjectToWorld._m03_m13_m23) * float3(1,0,1));
    const float3 planePoint = UNITY_MATRIX_M._m03_m13_m23;
    const float3 rayStart = _WorldSpaceCameraPos.xyz;
    const float3 rayDir = -normalize(mul(UNITY_MATRIX_I_V, float4(viewPos.xyz, 1.0)).xyz - rayStart);
    float dist = rayPlaneIntersection(rayDir, rayStart, planeNormal, planePoint);

    // calculate the clip space z for vertical plane
    float4 planeOutPos = mul(UNITY_MATRIX_VP, float4(rayStart + rayDir * dist, 1.0));
    float newPosZ = planeOutPos.z / planeOutPos.w * pos.w;

    // use the closest clip space z
    #if defined(UNITY_REVERSED_Z)
    pos.z = max(pos.z, newPosZ) + uv.z;
    #else
	pos.z = min(pos.z, newPosZ) + uv.z;
    #endif

    return pos;
}
            PackedVaryings CustomVertex(Attributes input)
            {
                Varyings output = (Varyings)0;
                output = BuildVaryings(input);
                output.positionCS = billboardMeshTowardsCamera(input.positionOS, _Offset, input.uv0);
                PackedVaryings packedOutput = (PackedVaryings)0;
                packedOutput = PackVaryings(output);
                return packedOutput;
            }

Other than that it works perfectly fine. How would I fix that? I tried compensating with float4(0.5,0.0,0.5,0.0) at several different places but no dice

Unsure of the approach going on, but perhaps the flame of that sprite must be centered in the UV space of the texture backing the sprite… and perhaps it isn’t centered?

That could be the reason I think but looking at the sprites they seem to be dead center unless the issue is when I create the mesh out of them.

public static Mesh BuildSpriteMesh(ACT.Frame frame, Sprite[] sprites, float alpha = 1) {
        meshBuildCount++;
        //Debug.Log("Building new mesh, current mesh count: " + meshBuildCount);

        outNormals.Clear();
        outVertices.Clear();
        outTris.Clear();
        outUvs.Clear();
        outColors.Clear();

        var mesh = new Mesh();

        var tIndex = 0;

        for(var i = 0; i < frame.layers.Length; i++) {
            var layer = frame.layers[i];

            if(layer.index < 0)
                continue;
            var sprite = sprites[layer.index];
            var verts = sprite.vertices;
            var uvs = sprite.uv;

            var rotation = Quaternion.Euler(0, 0, -layer.angle);
            var scale = new Vector3(layer.scale.x * (layer.isMirror ? -1 : 1), layer.scale.y, 1);
            
            for(var j = 0; j < verts.Length; j++) {
                var v = rotation * (verts[j] * scale);
                outVertices.Add(v + new Vector3(layer.pos.x, -(layer.pos.y)) / SPR.PIXELS_PER_UNIT);
                outUvs.Add(new Vector3(uvs[j].x, uvs[j].y, i));

                var c = new Color(layer.color.r, layer.color.g, layer.color.b, layer.color.a * alpha);

                outColors.Add(c);
                outNormals.Add(new Vector3(0, 0, -1));
            }

            if(layer.isMirror) {
                outTris.Add(tIndex + 2);
                outTris.Add(tIndex + 1);
                outTris.Add(tIndex);
                outTris.Add(tIndex + 2);
                outTris.Add(tIndex + 3);
                outTris.Add(tIndex + 1);
            } else {
                outTris.Add(tIndex);
                outTris.Add(tIndex + 1);
                outTris.Add(tIndex + 2);
                outTris.Add(tIndex + 1);
                outTris.Add(tIndex + 3);
                outTris.Add(tIndex + 2);
            }


            tIndex += 4;
        }

        //Debug.Log($"{outVertices.Count} {outColors.Count}");

        mesh.vertices = outVertices.ToArray();
        mesh.SetUVs(0,outUvs.ToArray());
        mesh.triangles = outTris.ToArray();
        mesh.colors = outColors.ToArray();
        mesh.normals = outNormals.ToArray();

        mesh.Optimize();

        return mesh;
    }

This is one of the sprites
image

I don’t deal with the Sprite graphics but if you sum all the vertices then divide them by how many to get the average position, does it equal (0,0)? Likely not so you might need to also find the center using its bounds then subtract that from the vertices meaning you’ll get the vertices centered around its local origin.

You can sort of see this being done here: Unity - Scripting API: Sprite.vertices

I hope I’m not completley wrong in this as it’s kind of a guess but I can consult other 2D team members for you.

That seems to be the case, indeed… When I average it yields (-0.28, 2.02, 0.00) however when I subtract the center from them the result is the same. I guess because the center/pivot of the sprite is normalized?

            Vector2 bounds = sprite.bounds.center;

            for (var j = 0; j < verts.Length; j++)
            {
                var rotatedVertex = rotation * ((verts[j] - bounds) * scale);
                outVertices.Add(rotatedVertex + new Vector3(layer.pos.x, -layer.pos.y) / SPR.PIXELS_PER_UNIT);

It seems I was wrong above, probabaly because I didn’t think it through and it’s not my area but apparently the vertices are already in “pivot space” so the vertex origin here is the pivot. If that’s actually the case, it would suggest your specified pivot in the editor is at that location. Is that true?

I did some further investigation before leaving to work and it seems that as long as i consider only the sprites vertices, the average is (0,0,0). The problem with that is each layer has it’s own positioning so it can compose multiple sprites on top of each other and if I remove that layer position

outVertices.Add(v + new Vector3(layer.pos.x, -(layer.pos.y)) / SPR.PIXELS_PER_UNIT);

It results on the sprites being laid down on the bottom of the whole quad…

That layer position is key to positioning elements so I’m not sure I’d work around this. I tried getting the sprite rect which was like 32x32 and subtracting by the position which is something like (-11.9,-2.3) but it just moves the entire sprite around.

Maybe if I take the average result and offset it on the shader? :thinking:

Backing up to first principles here, if I read your first post correctly, the entire point of any of this is because you don’t want a flame card (quad) clipping into stuff because it looks bad.

How does this shader help? Does it draw regardless of Z? Does it do something else? My thinking is that perhaps another approach would be better, something like a two-camera see-through-walls solution (with layers / layermasks) coupled with some kind of directional check to inhibit the flame when you’re on the other side of the wall completely…

The shader function is what I use for both my characters and the effects because simply copying the camera rotation was making it so the character would clip into meshes, I’ve also tried scaling the sprite height to fake perspective but that was causing some weird distortions so I opted for the shader solution instead.

The thing is, even if I remove the shader completely and use a mono to mimick only the camera’s Y rotation I can observe the same behaviour. Which leads me to think the issue is caused by the position offset when creating the quads.

to me this just looks like the consequence of warping caused from perspective mixed with objects that dont get warped from perspective (billboarded sprites)

since you are doing perfect billboarding, there is no chance that it is going to line up perfectly at every angle, you will have to manually add an adjustment based on the rotation and field of view, this might end up being just a “magic number” that somehow works

you could theoretically use math to calculate the distortion on the pillars (after all the engine is already doing it to be able to render it) but you probably want to keep your sanity

to test this notion you can just create a red circle quad, put it on the floor below the feet of your character, rotate the camera and you will see that your character too will get offcenter off the square

in addition, i suspect that you will have similar effects if you just move west to east, your flames will probably look offcenter as you get further from them (especially if you increased the field of view, i see that you currently have 15, but the higher the value, the worse it will get)

That seems to explain the issue, thank you!

However I could notice the characer being in the dead center of the camera pov because of LookAt the displacement on top of the quad is much less noticeable… Wonder if there’s any way around it or at least to make it feel less noticeable

well, the issue is the farther you get from the center of the screen, the more displacement you get

if you want to really reduce displacement you could just put the field of view to very low value

but that takes away the allure of being 3d, so in the end you will have to adjust the positions with some magic number that somehow works, corrected based on the transform forward of the camera