gonetz / GLideN64

A new generation, open-source graphics plugin for N64 emulators.
Other
776 stars 180 forks source link

Alpha blending emulation #2275

Closed ghost closed 3 years ago

ghost commented 4 years ago

In current master the alpha output of the color combiner is blent with memory alpha using the same source factor and destination factor in OpenGL's color blend equation.

I believe angrylions software plugin uses the following code in finalize_spanalpha() to write to the framebuffer.

    switch(other_modes.cvg_dest)
    {
    case CVG_CLAMP: 
        if (!blend_en)
        {
            finalcvg = curpixel_cvg - 1;

        }
        else
        {
            finalcvg = curpixel_cvg + curpixel_memcvg;
        }

        if (!(finalcvg & 8))
            finalcvg &= 7;
        else
            finalcvg = 7;

        break;
    case CVG_WRAP:
        finalcvg = (curpixel_cvg + curpixel_memcvg) & 7;
        break;
    case CVG_ZAP: 
        finalcvg = 7;
        break;
    case CVG_SAVE: 
        finalcvg = curpixel_memcvg;
        break;
    }

After the color combiner takes place, pixel alpha might be multiplied to pixel coverage if cvg_times_alpha flag is set. So curpixel_cvg might actually contain pixel alpha.

The first and second cases add pixels coverage to memory coverage. This may be used to simply write pixel coverage into memory or for antialiasing purposes for pixels where different polygons meet. CVG_WRAP is probably the most problematic one to emulate because OpenGL's blending doesn't support wrapping (I believe) and games may initialize memory coverage to 0xFF to automatically subtract one from pixel coverage (because (pixel.cvg + 0xFF) & 0x7 = pixel.cvg - 1).

The third and four cases should be straightforward to implement and might be critical for some framebuffer effects to show up.

Since pixel coverage calculation isn't emulated, it may be defaulted to 1.0 for the time being.

Of course, this is all theoretical, so we'll have to see how it goes and if it brings results. Wish me luck!

gonetz commented 4 years ago

Yes, currently alpha blending emulation is rather missing. I also checked angrylions plugin code and found that proper alpha blending requires coverage information. Lack of that functionality caused several issues, which I fixed with special hacks. I suppose that issue #728 is also related to alpha blending. @standard-two-simplex, good luck with it!

ghost commented 4 years ago

proper alpha blending requires coverage information

Yes that is true. I was hoping, however, that an accurate coverge value is used only for antialiasing purposes and that I could get away with assuming cvg = 1.0 for non-discarded pixels. This way alpha blending woul be partially supported.

728 is also related to alpha blending

It seems Pokemon stadium uses alpha values written to framebuffer very often. The black bars use mode CVG_FULL and thus show in my WIP code.

Mario Tennis also seems to use this alpha values a lot, but the red explosion doesn't show as it uses mode CVG_WRAP and alpha value of 0.0. Those fragments are discarded by the alpha test when rendering the auxiliary buffer to main buffer.

ghost commented 4 years ago

I can't figure out Mario Tennis explosions effects.

Correct effect achieved via hacks

The alpha test is used to decide whether it is shown with a constant threshold around 0.063. Currently, the explosions don't pass this test because the fragments have an alpha of 0.0.

I'm not very sure how the effect is working, but an auxiliary buffer is cleared in fill mode, then I guess the explosion texture is rendered over it and at last it is rendered in 1cycle mode to the main buffer.

In 1a9d370, hacking into fill color alpha makes the explosions visible because CVG_WRAP is used and the clear alpha is passed to the call rendering the auxiliary buffer into the main buffer. I haven't found much information about the rendering of flame textures.

Playing around with uCvGXAlpha hasn't resulted in any improvements.

I have spent lots of time on this and I'm quite tired about it, so I put it in a decent shape where cases CVG_DST_CLAMP and CVG_DST_WRAP use pixel alpha like master and cases CVG_DST_FULL and CVG_DST_SAVE are emulated.

I would expect no regressions and at least the warning sign in Pokemon stadium is fixed.

https://github.com/standard-two-simplex/GLideN64/tree/blender_changes

Jj0YzL5nvJ commented 4 years ago

Maybe @Themaister can help you understand what Mario Tennis is doing behind the curtain. He is optimizing that case in paraLLEl-RDP.

olivieryuyu commented 4 years ago

tested one game and got a bug

GLideN64_Battle_for_Naboo_007

see how the letters are now

ghost commented 4 years ago

battle_for_naboo-000

This is how I see them. Can you post your graphic settings so I can compare to mine?

Edit: Nevermind, it's native rects as gonetz pointed out in the PR.

olivieryuyu commented 4 years ago

yes u are right, something odd must have happened

gonetz commented 4 years ago

@standard-two-simplex could you give me a patch with your hack for Mario Tennis? I can't guess, how to hack it to get the explosion visible.

ghost commented 4 years ago

@standard-two-simplex could you give me a patch with your hack for Mario Tennis?

Here 00278ce0563a9a6d7675df5057dd3177f8a19ef2

The auxiliary framebuffer is drawn in cvg_wrap mode (uCvgDest ==1). The alpha held by clampedColor.a is 0.0. When the auxiliary buffer is rendered to the main buffer, it fails the alpha test, which has a threshold somewhere around 0.065

ghost commented 4 years ago

Also the textures which are used to render the explosion look like this MarioTennis#07F61D3A#2#1#B88E3934_ciByRGBA which have non transparent background. The black part never appeared in the screen in my tries, so I suspect some strange blending is happening.

gonetz commented 4 years ago

which have non transparent background. The black part never appeared in the screen in my tries, so I suspect some strange blending is happening.

You are right and wrong about some strange blending. So, what I found about the explosion.

The explosion texture has format 8bit CI. It is totally opaque. Even black texels have full alpha.

Step 1: render explosion textures into an aux buffer. Texrect, 1 cycle Color combiner: (TEXEL0 - 0) PRIMITIVE + 0 Alpha combiner (1 - 0) TEXEL0 + PRIMITIVE Primitive color is just 0xFFFFFFFF, so output color is TEXEL0, output alpha is TEXEL0 + 0xFF. Since texel alpha is 0xFF, output alpha is 0xFF 2 = 0x1FE Blender is enabled, but blender equation is in 0 + in * 1, that is it just passes the color to the buffer. We should have full alpha in the buffer, since 0x1FE would be clamped to 0xFF. cvg_wrap mode is set here, which in our code means just combiner alpha. But there is also such thing as "N64 color wrap and clamp". It wraps our 0x1FE to zero, so all pixels in the aux buffer have zero alpha. Your hack overrides it.

Step 2 render aux buffer to the main one. Again with texrect, but 2 cycle this time. Blender is enabled, but again it uses the simple pass-through equation in both cycles: in 0 + in 1, So, the blender is not responsible for the transparency here. Alpha test is also enabled. It discards the explosion when texels in aux buffer texture have zero alpha. When the texels have full alpha it does nothing. I removed alpha test completely just to check, what it does here. Lots of things got broken but the explosion did not change a bit.

The interesting part here is the combiner.

Color combiner: cycle 1 (TEXEL0 - 0) PRIMITIVE_ALPHA + 0 cycle 2 (NOISE - TEXEL0) COMBINED + TEXEL0

Alpha combiner: cycle 1 0, 0, 0, TEXEL0 cycle 2 0, 0, 0, COMBINED

resulted alpha is just texel alpha from the aux buffer texture. It has no interest for us since the blender equation does not use it.

Primitive color is again 0xFFFFFFFF, so the result of the first cycle of color combiner is TEXEL0, that is color of texel from the aux buffer. As you know, TEXEL0 in the second cycle of the color combiner means TEXEL1, that is the second texture. The first texture is the aux buffer. The second texture is (surprise!) the main color buffer where we are currently rendering! That is the second cycle of the color combiner works as a blender: it blends the aux buffer texture with the current buffer. When texel of aux buffer texture is black the result is just main buffer color. Actually, dark texels of explosion texture are not black. They are rather dark grey. Thus they are not disappear completely after the "blending". Dark area of the explosion texture is barely visible but still visible: explosion Btw, I'm not sure how it works with OpenGL, since it is prohibited to use the same texture as input and output one for the same shader (shader blender). Unless I made a special code for that and forgot about it.

Since "N64 color wrap and clamp" turns alpha in the Step 1 to zero, another way to hack this effect is to replace " lowp vec4 wrappedColor = WRAP(cmbRes, -0.51, 1.51); \n" by " lowp vec4 wrappedColor = cmbRes; \n"

The open question is how to make the explosion work without hacks?

ghost commented 4 years ago

Alpha test is also enabled. It discards the explosion when texels in aux buffer texture have zero alpha. When the texels have full alpha it does nothing. I removed alpha test completely just to check, what it does here. Lots of things got broken but the explosion did not change a bit.

Do you mean that the alpha test is currently discarding all texels and the expected value is not to discard any texel, whether its black or orange?

We should have full alpha in the buffer, since 0x1FE would be clamped to 0xFF. cvg_wrap mode is set here, which in our code means just combiner alpha. But there is also such thing as "N64 color wrap and clamp". It wraps our 0x1FE to zero, so all pixels in the aux buffer have zero alpha. Your hack overrides it.

I check angrylions code. It's doing ((a-b)*c + (d<<8) + 0x80)>>8 after sign-extending a,b,c,d. The addition of 0x80 seems to be rounding to closest rather than down.

In fixed point arithmetic, the alpha equation (1-0)*TEXEL0+PRIMITIVE should be calculating,

(0xFF - 0x00) * 0xFF + 0xFF00 + 0x80 = 0xFE01 + 0xFF00 + 0x80 = 0x1FD81

after the bit shift it becomes 0x1FD which clamps to 0x0 as you mentioned.

In floating point arithmetic, we' re doing (if I'm not mistaken)

(1.0 - 0.0) * 1.0 + 1.0 = 2.0

which is wrapped to 0.0 so it should be accurate enough.

In principle, CVG_WRAP means adding combined alpha to memory alpha and keeping the last 3 bits. Since, pixel alpha is 0.0 this should preserve previous memory alpha. Thus it keeps the alpha of the FillRect() command that cleared the auxiliary buffer.

This is currently emulated, but the auxiliary buffer alpha is cleared to 0.0. If you hack gDPFillRectangle() to set gDP.rectColor.a = 1.0 (or any value that will pass the alpha test) the explosions also show. I don't know which color angrylions code clears the auxiliary buffer to.

The cycle1 of the alpha combiner when rendering into main buffer does (0-0)*0+TEXEL0, so the alpha output is 0.0. At this stage the alpha test fails and the explosion fragments are discarded.

I don't know which step is incorrect. Color and alpha clamp are supposed to happen, the combiner equation outputs looks correct, and the fillrect and alpha test code is pretty straightforward.

cycle 2 (NOISE - TEXEL0) * COMBINED + TEXEL0

Is this combined color or alpha?

The equation is quite strange. It linearly interpolates between TEXEL0 and NOISE (random rgb?) using combined (color or alpha?) as interpolation factor. I guess it may explain why the black pixels disappear, but I can't understand what it does otherwise.

gonetz commented 4 years ago

Do you mean that the alpha test is currently discarding all texels

Yes

and the expected value is not to discard any texel, whether its black or orange?

I'm not sure, but I don't see how resulted alpha can differ from 0 or 1. As I can see, alpha test discards either all or nothing here.

In principle, CVG_WRAP means adding combined alpha to memory alpha and keeping the last 3 bits.

May be CVG_WRAP means adding combined cvg to memory cvg and keeping the last 3 bits?

Is this combined color or alpha?

Combined color. It is color from the aux buffer texture in our case.

The equation is quite strange. It linearly interpolates between TEXEL0 and NOISE (random rgb?) using combined (color or alpha?) as interpolation factor. I guess it may explain why the black pixels disappear, but I can't understand what it does otherwise.

If COMBINED is black, the result is main buffer color. If COMBINED is white, the result is some random color. Otherwise the result is something in between main color and random color. I also don't quite understand it, but I guess that explosion looks so dynamic because of randomness.

ghost commented 4 years ago

May be CVG_WRAP means adding combined cvg to memory cvg and keeping the last 3 bits?

If CVG_X_ALPHA == 0, then pixel coverage is used, If CVG_X_ALPHA == 1 then, pixel coverage times pixel alpha is used. Notice that pixel coverage is most of the times 0xFF, so this means pixel alpha is used in the latter case.

I had tried writing lowp float cvg = uCvgXAlpha == 0 ? 1.0 ? clampedColor.a; in ShaderBlenderAlpha() but the explosions don't appear and I'm not sure if I encountered regressions.

ghost commented 3 years ago

Closing this as alpha blending is emulated. I'll open another issue about Mario Tennis.