Welcome, Guest.
Username: Password: Remember me

TOPIC: Orton Bloom

Orton Bloom 5 months 2 weeks ago #1

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.
#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:
Last Edit: 5 months 1 week ago by moriz1.
The administrator has disabled public write access.
The following user(s) said Thank You: Androll, WalterDasTrevas, Rudy102, Zarathustra

Orton Bloom 5 months 2 weeks ago #2

As far as I know reverse tonemapping is literally taking a tonemapping shader and flipping the signes so + is - and - is plus.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #3

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.
Likes to reinvent the wheel.

My shaders repository: www.github.com/luluco250/FXShaders
Last Edit: 5 months 1 week ago by luluco250.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #4

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.
Ooh new toy! Thank you.
I don't have time to test at the moment, could you (or someone else) post before/after screen shots?
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #5

i don't have screenshots, but i do have a video!
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #6

moriz1 wrote:
(...) then blend using the "screen" blend mode to blend it back into the original image. (...)
Nice effect, but a lot of details get lost in the bright areas.
Can you experiment with different blending modes? Simple Lerp or some gamma-type blend like overlay, to not clip whites?
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #7

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?
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #8

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.
Likes to reinvent the wheel.

My shaders repository: www.github.com/luluco250/FXShaders
Last Edit: 5 months 1 week ago by luluco250.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #9

Scalars are automatically expanded, and for good measure ReShade converts myvar.xyz - 1.0 to myvar.xyz - float3(1.0,1.0,1.0) when translating ReShade FX to HLSL before compiling it (myvar.xyz - vec3(1.0) for GLSL). This only applies to vectors and scalars, so a float3 * a float2 won't work.
Last Edit: 5 months 1 week ago by Marty McFly.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #10

moriz1 wrote:
i don't have screenshots, but i do have a video!
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.

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.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #11

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.
This!! :lol: I need it too, please, make it happen :)
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #12

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.
Likes to reinvent the wheel.

My shaders repository: www.github.com/luluco250/FXShaders
Last Edit: 5 months 1 week ago by luluco250.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #13

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.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #14

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?
Last Edit: 5 months 1 week ago by moriz1.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #15

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.
Likes to reinvent the wheel.

My shaders repository: www.github.com/luluco250/FXShaders
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #16

i got it! inverse reinhard, and a variable cutoff that gets modified by (un-inverted) luma to create this bloom texture:



it then gets blended with screen blending with the same un-inverted luma from above as blending strength.
Last Edit: 5 months 1 week ago by moriz1.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #17

i got it! inverse reinhard, and a variable cutoff that gets modified by (un-inverted) luma to create this bloom texture:
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.
it then gets blended with screen blending with the same un-inverted luma from above as blending strength.
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:
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;
Likes to reinvent the wheel.

My shaders repository: www.github.com/luluco250/FXShaders
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #18

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.
Last Edit: 5 months 1 week ago by moriz1.
The administrator has disabled public write access.

Orton Bloom 5 months 1 week ago #19

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.
Likes to reinvent the wheel.

My shaders repository: www.github.com/luluco250/FXShaders
The administrator has disabled public write access.
The following user(s) said Thank You: Scorpio82CO