Orton Bloom
- moriz1
- Topic Author
about 30 seconds into implementation, i suddenly realized: "hey wait a second, this is literally bloom!" and so, OrtonBloom.fx is born.
#ifndef DOWNSCALE_BLUR_RES
#define DOWNSCALE_BLUR_RES 1 // [0 to 3] Change to value higher than 0 to improve performance at high resolutions
// Recommended for 4K: 1 or 2. Leave at 0 for lower resolutions
#endif
uniform bool GammaCorrectionEnable <
ui_label = "Enable Gamma Correction";
toggle = true;
> = true;
uniform float BlurMulti <
ui_label = "Blur Multiplier";
ui_type = "drag";
ui_min = 0.0; ui_max = 1.0;
ui_tooltip = "Blur strength";
> = 1.0;
uniform int BlackPoint <
ui_type = "drag";
ui_min = 0; ui_max = 255;
ui_tooltip = "The new black point for blur texture. Everything darker than this becomes completely black.";
> = 60;
uniform int WhitePoint <
ui_type = "drag";
ui_min = 0; ui_max = 255;
ui_tooltip = "The new white point for blur texture. Everything brighter than this becomes completely white.";
> = 150;
uniform float MidTonesShift <
ui_type = "drag";
ui_min = -1.0; ui_max = 1.0;
ui_tooltip = "Adjust midtones for blur texture.";
> = -0.84;
uniform float BlendStrength <
ui_label = "Blend Strength";
ui_type = "drag";
ui_min = 0.00; ui_max = 1.0;
ui_tooltip = "Opacity of blur texture. Keep this value low, or image will get REALLY blown out.";
> = 0.07;
#include "ReShade.fxh"
texture GaussianBlurTex { Width = BUFFER_WIDTH / (1 + DOWNSCALE_BLUR_RES); Height = BUFFER_HEIGHT / (1 + DOWNSCALE_BLUR_RES); Format = RGBA8; };
sampler GaussianBlurSampler { Texture = GaussianBlurTex; };
texture GaussianBlurTex2 { Width = BUFFER_WIDTH / (1 + DOWNSCALE_BLUR_RES); Height = BUFFER_HEIGHT / (1 + DOWNSCALE_BLUR_RES); Format = RGBA8; };
sampler GaussianBlurSampler2 { Texture = GaussianBlurTex2; };
float CalcLuma(float3 color) {
if (GammaCorrectionEnable) {
return pow((color.r*2 + color.b + color.g*3) / 6, 1/2.2);
}
return (color.r*2 + color.b + color.g*3) / 6;
}
float3 GaussianBlur1(in float4 pos : SV_Position, in float2 texcoord : TEXCOORD) : SV_Target
{
float3 color = tex2D(ReShade::BackBuffer, texcoord).rgb;
float blurPower = CalcLuma(color);
float offset[18] = { 0.0, 1.4953705027, 3.4891992113, 5.4830312105, 7.4768683759, 9.4707125766, 11.4645656736, 13.4584295168, 15.4523059431, 17.4461967743, 19.4661974725, 21.4627427973, 23.4592916956, 25.455844494, 27.4524015179, 29.4489630909, 31.445529535, 33.4421011704 };
float weight[18] = { 0.033245, 0.0659162217, 0.0636705814, 0.0598194658, 0.0546642566, 0.0485871646, 0.0420045997, 0.0353207015, 0.0288880982, 0.0229808311, 0.0177815511, 0.013382297, 0.0097960001, 0.0069746748, 0.0048301008, 0.0032534598, 0.0021315311, 0.0013582974 };
color *= weight[0];
[loop]
for(int i = 1; i < 18; ++i)
{
color += tex2D(ReShade::BackBuffer, texcoord + float2(offset[i] * ReShade::PixelSize.x, 0.0) * blurPower * BlurMulti).rgb * weight[i];
color += tex2D(ReShade::BackBuffer, texcoord - float2(offset[i] * ReShade::PixelSize.x, 0.0) * blurPower * BlurMulti).rgb * weight[i];
}
return saturate(color);
}
float3 GaussianBlur2(in float4 pos : SV_Position, in float2 texcoord : TEXCOORD) : SV_Target
{
float3 color = tex2D(GaussianBlurSampler, texcoord).rgb;
float3 original = tex2D(ReShade::BackBuffer, texcoord).rgb;
float blurPower = CalcLuma(color);
float offset[18] = { 0.0, 1.4953705027, 3.4891992113, 5.4830312105, 7.4768683759, 9.4707125766, 11.4645656736, 13.4584295168, 15.4523059431, 17.4461967743, 19.4661974725, 21.4627427973, 23.4592916956, 25.455844494, 27.4524015179, 29.4489630909, 31.445529535, 33.4421011704 };
float weight[18] = { 0.033245, 0.0659162217, 0.0636705814, 0.0598194658, 0.0546642566, 0.0485871646, 0.0420045997, 0.0353207015, 0.0288880982, 0.0229808311, 0.0177815511, 0.013382297, 0.0097960001, 0.0069746748, 0.0048301008, 0.0032534598, 0.0021315311, 0.0013582974 };
color *= weight[0];
[loop]
for(int i = 1; i < 18; ++i)
{
color += tex2D(GaussianBlurSampler, texcoord + float2(0.0, offset[i] * ReShade::PixelSize.y) * blurPower * BlurMulti).rgb * weight[i];
color += tex2D(GaussianBlurSampler, texcoord - float2(0.0, offset[i] * ReShade::PixelSize.y) * blurPower * BlurMulti).rgb * weight[i];
}
return saturate(color);
}
float3 LevelsAndBlend(float4 vpos : SV_Position, float2 texcoord : TEXCOORD) : SV_Target
{
float black_point_float = BlackPoint / 255.0;
float white_point_float = WhitePoint == BlackPoint ? (255.0 / 0.00025) : (255.0 / (WhitePoint - BlackPoint)); // Avoid division by zero if the white and black point are the same
float mid_point_float = (white_point_float + black_point_float) / 2.0 + MidTonesShift;
if (mid_point_float > white_point_float) { mid_point_float = white_point_float; }
else if (mid_point_float < black_point_float) { mid_point_float = black_point_float; }
float3 color = tex2D(GaussianBlurSampler2, texcoord).rgb;
float3 original = tex2D(ReShade::BackBuffer, texcoord).rgb;
color = (color * white_point_float - (black_point_float * white_point_float)) * mid_point_float;
return saturate(max(0.0, max(original, lerp(original, (1 - (1 - saturate(color)) * (1 - saturate(color))), BlendStrength))));
}
technique OrtonBloom
{
pass GaussianBlur1
{
VertexShader = PostProcessVS;
PixelShader = GaussianBlur1;
RenderTarget = GaussianBlurTex;
}
pass GaussianBlur2
{
VertexShader = PostProcessVS;
PixelShader = GaussianBlur2;
RenderTarget = GaussianBlurTex2;
}
pass LevelsAndBlend
{
VertexShader = PostProcessVS;
PixelShader = LevelsAndBlend;
}
}
the idea is fairly simple: take a duplicate of the backbuffer, blur it, crush blacks and blow out highlights, adjust midtones downward to boost contrast some more, then blend using the "screen" blend mode to blend it back into the original image.
i'm using gamma-corrected luma values to control the blur radius. it's probably better if i used a reverse tonemapper, but i don't understand the concept well enough, it seems.
EDIT: i forgot to include the demo. oops. here it is:
Please Log in or Create an account to join the conversation.
- MaxG3D
Please Log in or Create an account to join the conversation.
- luluco250
float3 reinhard(float3 c) {
return c / (1.0 + c);
}
float3 inv_reinhard(float3 c, float max_c) {
return (c / max(1.0 - c, max_c));
}
max_c is 1.0 / max_brightness, it being the maximum color to extract from the image. This is important because you don't want to get too much brightness, max_brightness = 100.0 is a good starting point.
Please Log in or Create an account to join the conversation.
- Martigen
Ooh new toy! Thank you.moriz1 wrote: a while ago, i was reading up on this old photography trick called the "Orton Effect" , a technique that photographers used to give their photos that extra "pop". so, i gave it a go to implement it in shader form.
about 30 seconds into implementation, i suddenly realized: "hey wait a second, this is literally bloom!" and so, OrtonBloom.fx is born.
I don't have time to test at the moment, could you (or someone else) post before/after screen shots?
Please Log in or Create an account to join the conversation.
- moriz1
- Topic Author
Please Log in or Create an account to join the conversation.
Nice effect, but a lot of details get lost in the bright areas.moriz1 wrote: (...) then blend using the "screen" blend mode to blend it back into the original image. (...)
Can you experiment with different blending modes? Simple Lerp or some gamma-type blend like overlay, to not clip whites?
Please Log in or Create an account to join the conversation.
- moriz1
- Topic Author
luluco250 wrote: I'm no math guy but I jury-rigged this inverse of Reinhard for my bloom shader(s), see if it helps you.
float3 reinhard(float3 c) { return c / (1.0 + c); } float3 inv_reinhard(float3 c, float max_c) { return (c / max(1.0 - c, max_c)); }
max_c is 1.0 / max_brightness, it being the maximum color to extract from the image. This is important because you don't want to get too much brightness, max_brightness = 100.0 is a good starting point.
how exactly does 1.0 - c work? 1.0 is a float, while c is a float3. i thought you can't do scalar - matrix? is hlsl converting 1 into a matrix in this case?
Please Log in or Create an account to join the conversation.
- luluco250
how exactly does 1.0 - c work?
CG/HLSL/FX is more lenient on implicit scalar->vector conversions, which I prefer. Writing vec3(1.0) isn't fun after the 100th time.
Oh and also, from my personal testing, you might want to perform the actual reverse tonemapping on the luminance/brightness of the image (can be acquired with max(color.r, max(color.g, color.b))) then multiply by the original color to get the result, which seems to eliminate some color artifacts caused by the inherent lack of precision of the operation.
Please Log in or Create an account to join the conversation.
- Marty McFly
Please Log in or Create an account to join the conversation.
- Martigen
Really nice! Looks really great for distance, blurring the image as the bloom envelops it! One thing I did notice though is the rocks in the lower left also, naturally, become blurred and out of focus as the brightness/bloom increased.moriz1 wrote: i don't have screenshots, but i do have a video!
Would it be possible to add a depth-function that increases the effect the further away? This might allow for eg that distance bloomed sky with blurring while not affecting bright objects closer to the camera.
Please Log in or Create an account to join the conversation.
- Androll
This!! I need it too, please, make it happenWould it be possible to add a depth-function that increases the effect the further away? This might allow for eg that distance bloomed sky with blurring while not affecting bright objects closer to the camera.
Please Log in or Create an account to join the conversation.
- luluco250
Would it be possible to add a depth-function that increases the effect the further away? This might allow for eg that distance bloomed sky with blurring while not affecting bright objects closer to the camera.
It's pretty straightforward to do, you can simply multiply the intensity by the depth (while also adding 1.0 to avoid reducing it rather than increasing it. I've done it before and it looks pretty good, though sometimes arbitrary. Also remember that skies aren't extremely bright IRL unlike certain games like FNV.
Please Log in or Create an account to join the conversation.
- Marty McFly
MaxG3D wrote: As far as I know reverse tonemapping is literally taking a tonemapping shader and flipping the signes so + is - and - is plus.
No. Tonemappers are basically tonemappedcolor = f(untonemappedcolor). So y = f(x). Inverse tonemapper is solving the tonemapping function for x (so you get y from x instead x from y). Some functions are not reversible though.
Please Log in or Create an account to join the conversation.
- moriz1
- Topic Author
luluco250 wrote:
Would it be possible to add a depth-function that increases the effect the further away? This might allow for eg that distance bloomed sky with blurring while not affecting bright objects closer to the camera.
It's pretty straightforward to do, you can simply multiply the intensity by the depth (while also adding 1.0 to avoid reducing it rather than increasing it. I've done it before and it looks pretty good, though sometimes arbitrary. Also remember that skies aren't extremely bright IRL unlike certain games like FNV.
the best way to do this, is to introduce depth at the last step, and use it to modify the blend strength variable. in this scenario, you definitely do NOT want to add 1.0 to it, since it will cause it to go out of bounds for the lerp function.
i've also quickly realized that the Orton effect, despite being very similar to bloom (and might actually be a precursor to bloom), it is NOT actually bloom. as such, using inverse tonemapping might not be appropriate. i've come to the point, where if i were to proceed, i would no longer be able to call this an Orton Effect shader anymore.
btw correction: i'm NOT using "screen" blending mode. in fact, i'm not really sure what blending mode i'm using, since a careful examination reveals that it matches no known blending formula that i can find. it's doing... something?
Please Log in or Create an account to join the conversation.
- luluco250
i've also quickly realized that the Orton effect, despite being very similar to bloom (and might actually be a precursor to bloom), it is NOT actually bloom. as such, using inverse tonemapping might not be appropriate. i've come to the point, where if i were to proceed, i would no longer be able to call this an Orton Effect shader anymore.
btw correction: i'm NOT using "screen" blending mode. in fact, i'm not really sure what blending mode i'm using, since a careful examination reveals that it matches no known blending formula that i can find. it's doing... something?
Your effect seems more to me like a depth of field smoothly blended with bloom, you see this a lot in old UE3 games like ME1 because they share the same texture for bloom and dof.
Please Log in or Create an account to join the conversation.
- moriz1
- Topic Author
it then gets blended with screen blending with the same un-inverted luma from above as blending strength.
Please Log in or Create an account to join the conversation.
- luluco250
Nice, so are you using inverse reinhard on the luma/luminance then multiplying by the LDR color or just applying it to the color directly? Like I said, I personally got better results with the former, but only doing it on the bloom texture.i got it! inverse reinhard, and a variable cutoff that gets modified by (un-inverted) luma to create this bloom texture:
You can then blend it with the unblurred image via addition after running it through the regular inverse tonemapper. Instead of getting some color artifacts, colors that are too bright become white. Like this:it then gets blended with screen blending with the same un-inverted luma from above as blending strength.
float3 reinhard(float3 c) {
return c / (1.0 + c);
}
float3 inv_reinhard(float3 c, float max_c) {
return (c / max(1.0 - c, max_c));
}
float3 inv_reinhard_preserve_colors(float3 c, float max_c) {
float lum = max(c.r, max(c.g, c.b));
return c * (lum / max(1.0 - lum, max_c));
}
// in the brightpass/makehdr shader
float3 color = tex2D(backbuffer, uv).rgb;
color = inv_reinhard_preserve_colors(color);
// in the final/blend shader
float3 bloom = tex2D(bloom, uv).rgb;
float3 color = tex2D(backbuffer, uv).rgb;
color = inv_reinhard(color);
color += bloom * intensity;
Please Log in or Create an account to join the conversation.
- moriz1
- Topic Author
luluco250 wrote: Nice, so are you using inverse reinhard on the luma/luminance then multiplying by the LDR color or just applying it to the color directly? Like I said, I personally got better results with the former, but only doing it on the bloom texture.
i'm using inverse reinhard on luma. i then specified a cutoff value, tossed everything that's below the cutoff, then stored everything above the cutoff as original LDR color. the benefit of this, is that anything that isn't specifically bright is pure black. with screen blending, these areas will simply have no effect on the final image, no matter how high i crank the blend strength.
luluco250 wrote: You can then blend it with the unblurred image via addition after running it through the regular inverse tonemapper. Instead of getting some color artifacts, colors that are too bright become white.
here i reused the levels adjustment that i had going previously. since the blur texture is generated from the original LDR colors, i can fine tune the exact look of the color adjustments.
Please Log in or Create an account to join the conversation.
- luluco250
here i reused the levels adjustment that i had going previously. since the blur texture is generated from the original LDR colors, i can fine tune the exact look of the color adjustments.
I see, what I like to do is add both the image and bloom in HDR color and then re-tonemap the result, gives a nice blending and looks like in-game bloom almost.
Please Log in or Create an account to join the conversation.