fabiangreffrath / crispy-doom

Crispy Doom is a limit-removing enhanced-resolution Doom source port based on Chocolate Doom.
https://fabiangreffrath.github.io/crispy-homepage
GNU General Public License v2.0
802 stars 132 forks source link

Transparency improvements #415

Open JNechaevsky opened 5 years ago

JNechaevsky commented 5 years ago

There are couple of things, which makes me worry in the transparency. But first of all, we need a map, not even a simple testing map, we need a galley with all transparent items (1), placed in three different light levels & backgrounds: light-gray-dark (2). Here it is: gallery.zip

1. About the items.

I'm using this approach:

// fixed map
vis->colormap[0] = vis->colormap[1] = fixedcolormap;
}
else if (thing->frame & FF_FULLBRIGHT && !(thing->flags & MF_TRANSLUCENT))
{

Have a look at this invul sphere (screenshot). It's too transparent, just like a … “soap bubble”. Or here, Supercharge is barely notable in the dark (screenshot). This should not happen.

With a additive approach some things becomes better, but not always, they are not looking so good in the bright areas (screenshot)

Suggested approach: reduce amount of transparency, let items be more opaque.

2. About the projectiles

Mostly same logic as above, but there is a difference. Have a look at non-additive fireball (screenshot)

Yes, it is transparent, but in fact, it’s not looking proper, no longer bright, as it should be. I’m have to agree, that additive transparency may looking to vargant, but must admit, that it’s a proper way to represent fire and plasma transparency by more realistic way.

Suggested approach: left additive approach for projectiles, but also reduce amount of transparency.

There is one more important vice-versa aspect here: exploded projectiles must not be too solid (but neither too transparent). Ultimately notable example - exploded Imp fireball above player face. If player is using transparency, he is expecting to see through this flame.

3. About the muzzle flashes.

Same problem. Muzzle flash must not looks like this (screenshot), ideally, it should be something like this (screenshot - bright, and just a very little transparent).

Suggested approach: maybe an additive transparency should be used, again, with reduced amount of transparency?


To summarize, most of all written above comes to me in RD, where I was trying to make two things:

  1. Really not notably, just a little bit transparent, but still transparent items,
  2. Making those items looking good in transparency of 256 colors.

This is just my vision, your decision should remain only your of course. Anyways, which of described above we should take care about first?

Suggestion №108

fabiangreffrath commented 5 years ago

Let's get the facts sorted: We have two types of translucency, alpha blending and additive blending. The latter is used for full-bright sprites (including the laser spot), the former for anything else. We agree that the current translucency value of blend_alpha = 0xa8 (which is 66% of 0xff) doesn't provide for optimal visibility of alpha-blended sprites. We should try to make them a bit more opaque by using values such as 0xc0 (75% of 0xff) or 0xcc (80% of 0xff).

This all applies to alpha blending. Now comes the tricky part: We don't even use an alpha value for additive blending! RGB values are simply added and capped at 0xff. So, how do we improve this algorithm?

One thing to keep in mind: Visibility through translucent objects is an issue, but at most a secondary one. Vanilla Doom had no translucency at all, so all objects were fully opaque and thus obscured the player's sight. So, any little impression of the game world behind a translucent sprite is already a net tactical improvement.

JNechaevsky commented 5 years ago

So, any little impression

You've got me absolutely right, thanks! Exactly a little, to keep original shape and volume of the objects. Additionally, though, there might be two possible levels, like:

Probably this will make everyone happy.

We don't even use an alpha value for additive blending!

What? How? :anguished: I wasn't know that, honestly. I have no any technical ideas about improvement, so... Maybe we just can do not use additive blending, considering only standard alpha blending for everything. This also will probably fits well with "one logic" approach, and will be safe for non-standard sprites, because we can't predict everything. And of course, this will make sprites looking good on white or bright backgrounds. And also, this way there will be much less work to do.

I'll probably again will be busy in the evening, but anyways, I definitely wants to play around with blend_alpha.

JNechaevsky commented 5 years ago

P.S. If you will have a free minute before deep evening (~23:00), could you please send a screenshot or two of blend_alpha 80% and 85%, with BFG ball fired on MAP13 (on the white floor texture)? I really wants to see this ASAP, but can't do even simple change in the code because of lack of time.

fabiangreffrath commented 5 years ago

Sorry, I don't think I can offer this today.

JNechaevsky commented 5 years ago

@fabiangreffrath I'm changing blend_alpha from 0x00 to 0xff and don't see any difference, even with && !(thing->flags & MF_TRANSLUCENT)) addition. Am I doing something wrong? Or something not ready yet in the code?

Also, there is a small visual glitch with changing gamma level while paused state, probably caused by reducing amount of red pain effect while paused state. Nothing critical, just attracted my attention. Note how rocket explosion "flashing" for one tic after changing gamma level:

fabiangreffrath commented 5 years ago

even with && !(thing->flags & MF_TRANSLUCENT)) addition.

Please don't do that! It only prevents translucent sprites from being rendered full-bright, but this didn't even remotely bring the desired effect. Please do not use this anymore, ever.

Am I doing something wrong? Or something not ready yet in the code?

You'll need to get rid of additive blending (which doesn't use this alpha value at all) entirely, e.g.

--- a/src/doom/r_things.c
+++ b/src/doom/r_things.c
@@ -802,7 +802,7 @@ void R_ProjectSprite (mobj_t* thing)
     // [crispy] translucent sprites
     if (thing->flags & MF_TRANSLUCENT)
     {
-       vis->blendfunc = (thing->frame & FF_FULLBRIGHT) ? I_BlendAdd : I_BlendOver;
+       vis->blendfunc = I_BlendOver;
     }
 #endif
 }
@@ -897,7 +897,7 @@ static void R_DrawLSprite (void)
     vis->translation = R_LaserspotColor();
 #ifdef CRISPY_TRUECOLOR
     vis->mobjflags |= MF_TRANSLUCENT;
-    vis->blendfunc = I_BlendAdd;
+    vis->blendfunc = I_BlendOver;
 #endif
     vis->xiscale = FixedDiv (FRACUNIT, xscale);
     vis->texturemid = laserspot->z - viewz;

Also, there is a small visual glitch with changing gamma level while paused state, probably caused by reducing amount of red pain effect while paused state. Nothing critical, just attracted my attention. Note how rocket explosion "flashing" for one tic after changing gamma level:

Hm, yes, needless to say that palette and gamma changing works quite a bit different in truecolor mode. Let's keep this in mind, but there are more important glitches to fix. :wink:

fabiangreffrath commented 5 years ago

With the change I posted above, I think translucency is already looking pretty fine!

JNechaevsky commented 5 years ago

Uh-oh, 0xcc (80%) is way too solid, it is barely possible to see anything through this. Unpredictable. I was expecting a bit different result, not so solid, but it is obvious - transparency looking different in 256 (remembered! it's a "two hundred fifty six"!) and in true color.

Even 0xc0 (75%) is looking a bit solid, but -- it seems to be good enough for "conservative" mode.

Initial 0xa8 is fine for "modern" mode. There is only one thing which makes me sad: in contrast of brightmaps, transparent sprites with this level is looking too dimmed (screenshot).

There have to be a way to lit transparent sprites w/o an additive blending... But I can't imagine it for now. Is it possible to draw sprite with primary alpha blending, and "lit up it just a little bit more" somehow?


Edit: Or maybe I was playing in 256 color mode too much, so my eyes are too used to see a more familiar transparency. I need some more time, first impression is not always correct. But anyways, true color is an awesome thing!

JNechaevsky commented 5 years ago

Duh, I can fall asleep, but it is too late for today to try this - Photoshop is offering various blending modes (screenshot). Some of them are, let's just say, making a layer brighter, some of them darker, some are mixing colors in different ways. I need to try to do this:

  1. Take a screenshot from the game, where half of picture is bright area, and another half is dark.
  2. Place an upscaled BFG ball.
  3. Play around with different blending modes.
  4. Ideally, there have to be exactly one layer of blending, or maximum two, to achieve a - 1) always bright sprite, 2) resonably transparent sprite. Should be done without any brightness/contrast correction of sprite itself.

Maybe that will be the Key we are seeking for. And even if we'll find that key, another question is - how it should be represented in the code. But at least, it's worth to try.

fabiangreffrath commented 5 years ago

how it should be represented in the code

https://developer.nvidia.com/content/transparency-or-translucency-rendering

fabiangreffrath commented 5 years ago

Naive implementation:

--- a/src/i_video.c
+++ b/src/i_video.c
@@ -95,6 +95,7 @@ static SDL_Texture *yelpane = NULL;
 static SDL_Texture *grnpane = NULL;
 static int pane_alpha;
 static unsigned int rmask, gmask, bmask, amask; // [crispy] moved up here
+static uint8_t rshift, gshift, bshift, ashift;
 static const uint8_t blend_alpha = 0xa8;
 extern pixel_t* colormaps; // [crispy] evil hack to get FPS dots working as in Vanilla
 #else
@@ -1475,6 +1476,11 @@ static void SetVideoMode(void)
         SDL_FillRect(argbbuffer, NULL, I_MapRGB(0x0, 0xff, 0x0));
         grnpane = SDL_CreateTextureFromSurface(renderer, argbbuffer);
         SDL_SetTextureBlendMode(grnpane, SDL_BLENDMODE_BLEND);
+
+        ashift = argbbuffer->format->Ashift;
+        rshift = argbbuffer->format->Rshift;
+        gshift = argbbuffer->format->Gshift;
+        bshift = argbbuffer->format->Bshift;
 #endif
         SDL_FillRect(argbbuffer, NULL, 0);
     }
@@ -1866,11 +1872,15 @@ const pixel_t I_BlendAdd (const pixel_t bg, const pixel_t fg)
 {
        uint32_t r, g, b;

-       if ((r = (fg & rmask) + (bg & rmask)) > rmask) r = rmask;
-       if ((g = (fg & gmask) + (bg & gmask)) > gmask) g = gmask;
-       if ((b = (fg & bmask) + (bg & bmask)) > bmask) b = bmask;
+       r = ((fg & rmask) >> rshift) + (((0xff - blend_alpha) * ((bg & rmask) >> rshift)) >> 8);
+       g = ((fg & gmask) >> gshift) + (((0xff - blend_alpha) * ((bg & gmask) >> gshift)) >> 8);
+       b = ((fg & bmask) >> bshift) + (((0xff - blend_alpha) * ((bg & bmask) >> bshift)) >> 8);

-       return amask | r | g | b;
+       if (r > 0xff) r = 0xff;
+       if (g > 0xff) g = 0xff;
+       if (b > 0xff) b = 0xff;
+
+       return amask | (r << rshift) | (g << gshift) | (b << bshift);
 }

 // [crispy] http://stereopsis.com/doubleblend.html
JNechaevsky commented 5 years ago

Naive implementation:

It looks... A bit better, like sprites becomes more opaque and a bit more bright.

From my side, I think I'm on right track. There are two blending modes, one is standard alpha (66% opacity), second is called "Color dodge" (100% opacity, yay!). BFG ball sprite is not touched, i.e. no corrections were made. But I need more backgrounds to test, like midtones and colored ones... Here it is: screenshot.

JNechaevsky commented 5 years ago

Tested with different light levels and background colors: screenshot (1.5 Mb, kinda huge, but I need to see all the pixels). Double-blending approach is same in all areas.

Is it we are seeking for? What's your opinion?


Edit: Try to open this mockup with Gimp, it will explain pretty much everything! PSD file is friendly with Gimp 2.10.8: DOOM0014.zip

fabiangreffrath commented 5 years ago

Looks good! Though, not everything that looks good on static images does so in moving frames. Let's see if I can ever get this implemented...

One more question, I am not sure I get this right: Now we have two layers, one with alpha blending and one with color dodge. But, how are these two layers combined?

JNechaevsky commented 5 years ago

Actually... I can make a video and place BFG ball with these blending modes there. :grinning: In the mockup, layers with BFG ball are simply placed in same coords, one layer on top of the other. In the code, though, it should be different:

  1. Apply color dodging routines first
  2. Apply alpha blending routines second

But all these calculations should be done with one sprite. The order is critically important, otherwise the sprite will turn into something awfully bright.

JNechaevsky commented 5 years ago

Oops, I wasn't clear enough in my answer.

If I'll combine all three layers into one (background + dodge + alpha), everything will be okay because all pixel data is all set. If I'll combine just two (dodge + alpha), transparency will gone, because blending mode will be different in fact.

In the engine everything should be calculated dynamically and simultaneously, all routines in one tic. I think so.

fabiangreffrath commented 5 years ago

Maybe this will prove helpful: https://photoblogstop.com/photoshop/photoshop-blend-modes-explained

fabiangreffrath commented 5 years ago

According to this article, color dodge won't work with integer R/G/B values because of the way math works. Consider this:

1.0 / 0.5 = 2.0

255 / 128 = 2
0xff / 0x80 = 0x02

Same calculation with the little difference that 2.0 means "over the top bright" and 0x02 means "dark close to black". :weary:

JNechaevsky commented 5 years ago

Wha-? :frowning: But it must be okay. Have a look, if I'll remove alpha blending sprites from top of dodge'ed, here's what will happen: http://priscree.ru/img/2dabfb7703eb18.png The trick is using two blending modes, not just one.

Ir I got something wrong? Is a katastrofa and we had to find another aproach?

fabiangreffrath commented 5 years ago

Na, this is just the reason why the code I drafted didn't immediately work. Basically it's just fixed-point math with FRACBITS = 8 and FRACUNIT = 256. I just have to wrap my head around it...