diff --git a/leenkx/Shaders/compositor_pass/compositor_pass.frag.glsl b/leenkx/Shaders/compositor_pass/compositor_pass.frag.glsl index 4dd8525..ac38670 100644 --- a/leenkx/Shaders/compositor_pass/compositor_pass.frag.glsl +++ b/leenkx/Shaders/compositor_pass/compositor_pass.frag.glsl @@ -625,4 +625,37 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5); #ifdef _CLUT fragColor = LUTlookup(fragColor, lutTexture); #endif + + +#ifdef _CDitheringBlueNoise + const float ditherStrength = ditherStrengthValue / 255.0; + float noise = ditherBlueNoiseStyle(gl_FragCoord.xy); + float noiseOffset = (noise - 0.5) * ditherStrength; + fragColor.rgb += noiseOffset; +#endif + +#ifdef _CDitheringWhiteNoise + const float ditherStrength = ditherStrengthValue / 255.0; + float noise = ditherWhiteNoise(gl_FragCoord.xy); + float noiseOffset = (noise - 0.5) * ditherStrength; + fragColor.rgb += noiseOffset; +#endif + +#ifdef _CDitheringOrderedBayer4x4 + const float ditherStrength = ditherStrengthValue / 255.0; + float noise = ditherOrderedBayer4x4(ivec2(gl_FragCoord.xy)); + float noiseOffset = (noise - 0.5) * ditherStrength; + fragColor.rgb += noiseOffset; +#endif + +#ifdef _CDitheringOrderedBayer8x8 + const float ditherStrength = ditherStrengthValue / 255.0; + float noise = ditherOrderedBayer8x8(ivec2(gl_FragCoord.xy)); + float noiseOffset = (noise - 0.5) * ditherStrength; + fragColor.rgb += noiseOffset; +#endif + + //fragColor.rgb = clamp(fragColor.rgb, 0.0, 1.0); + + } diff --git a/leenkx/Shaders/std/tonemap.glsl b/leenkx/Shaders/std/tonemap.glsl index 2d87c87..6258047 100644 --- a/leenkx/Shaders/std/tonemap.glsl +++ b/leenkx/Shaders/std/tonemap.glsl @@ -89,3 +89,52 @@ vec3 tonemapAgXFull(vec3 x) { x = clamp(x, 0.0, 1.0); return pow(x, vec3(1.0/2.2)); } + + +// Interleaved Gradient Noise (Pseudo-random, AKA Blue Noise style) +// Based on http://momentsingraphics.de/BlueNoise.html +float ditherBlueNoiseStyle(vec2 p) { + return fract(sin(dot(p.xy, vec2(12.9898, 78.233))) * 43758.5453123); +} + +// White Noise Dithering +float ditherWhiteNoise(vec2 p) { + return fract(sin(dot(p, vec2(12.9898, 4.1414))) * 43758.5453); +} + +// Ordered Dithering (4x4 Bayer Matrix) +float ditherOrderedBayer4x4(ivec2 p) { + const float bayer[16] = float[16]( + 0.0, 8.0, 2.0, 10.0, + 12.0, 4.0, 14.0, 6.0, + 3.0, 11.0, 1.0, 9.0, + 15.0, 7.0, 13.0, 5.0 + ); + int index = (p.x % 4) * 4 + (p.y % 4); + return bayer[index] / 16.0; +} + +// Ordered Dithering (8x8 Bayer Matrix) +float ditherOrderedBayer8x8(ivec2 p) { + const float bayer8x8[64] = float[64]( + 0.0, 32.0, 8.0, 40.0, 2.0, 34.0, 10.0, 42.0, + 48.0, 16.0, 56.0, 24.0, 50.0, 18.0, 58.0, 26.0, + 12.0, 44.0, 4.0, 36.0, 14.0, 46.0, 6.0, 38.0, + 60.0, 28.0, 52.0, 20.0, 62.0, 30.0, 54.0, 22.0, + 3.0, 35.0, 11.0, 43.0, 1.0, 33.0, 9.0, 41.0, + 51.0, 19.0, 59.0, 27.0, 49.0, 17.0, 57.0, 25.0, + 15.0, 47.0, 7.0, 39.0, 13.0, 45.0, 5.0, 37.0, + 63.0, 31.0, 55.0, 23.0, 61.0, 29.0, 53.0, 21.0 + ); + int index = (p.x % 8) * 8 + (p.y % 8); + return bayer8x8[index] / 64.0; +} + + +//vec3 applyDither(vec3 color, vec2 screenCoord) { +// float quantizationLevels = 255.0; +// float noise = randomDither(screenCoord); +// float noiseOffset = (noise - 0.5) / quantizationLevels; +// vec3 ditheredColor = color + noiseOffset; +// return clamp(ditheredColor, 0.0, 1.0); +//} diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index 7e5b707..fc1af0e 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -217,8 +217,12 @@ def build(): if rpdat.rp_compositornodes: assets.add_khafile_def('rp_compositornodes') compo_depth = False + # wrd.compo_defs += '' if rpdat.lnx_tonemap != 'Off': - wrd.compo_defs = '_CTone' + rpdat.lnx_tonemap + wrd.compo_defs += '_CTone' + rpdat.lnx_tonemap + if rpdat.lnx_dithering != 'Off': + wrd.compo_defs += '_CDithering' + rpdat.lnx_dithering + wrd.compo_defs += '_CDitheringStrength' if rpdat.rp_antialiasing == 'FXAA': wrd.compo_defs += '_CFXAA' if rpdat.lnx_letterbox: diff --git a/leenkx/blender/lnx/props_renderpath.py b/leenkx/blender/lnx/props_renderpath.py index 30d5942..6134b71 100644 --- a/leenkx/blender/lnx/props_renderpath.py +++ b/leenkx/blender/lnx/props_renderpath.py @@ -617,6 +617,20 @@ class LnxRPListItem(bpy.types.PropertyGroup): ('AgXSimple', 'AgX', 'AgX Implementation')], # ('AgXFull', 'AgX (Full)', 'AgX Full Implementation')], name='Tonemap', description='Tonemapping operator', default='Filmic', update=assets.invalidate_shader_cache) + lnx_dithering: EnumProperty( + items=[('Off', 'Off', 'Off'), + ('BlueNoise', 'Blue Noise', 'Blue Noise'), + ('WhiteNoise', 'White Noise', 'White Noise'), + ('OrderedBayer4x4', 'Ordered Bayer 4x4', 'Ordered Bayer 4x4'), + ('OrderedBayer8x8', 'Ordered Bayer 8x8', 'Ordered Bayer 8x8')], + name='Dithering', description='Dithering operator', default='Off') + lnx_dithering_strength: FloatProperty( + name="Dither Strength", + description="Strength of the dithering effect (applied as offset / 255.0)", + default=1.0, + min=0.0, + max=1000.0 + ) lnx_fisheye: BoolProperty(name="Fish Eye", default=False, update=assets.invalidate_shader_cache) lnx_vignette: BoolProperty(name="Vignette", default=False, update=assets.invalidate_shader_cache) lnx_vignette_strength: FloatProperty(name="Strength", default=0.7, update=assets.invalidate_shader_cache) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index 7825268..13cd854 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -1974,6 +1974,11 @@ class LNX_PT_RenderPathCompositorPanel(bpy.types.Panel): layout.enabled = rpdat.rp_compositornodes layout.prop(rpdat, 'lnx_tonemap') + layout.prop(rpdat, 'lnx_dithering') + if rpdat.lnx_dithering != 'Off': + row = layout.row(align=True) + row.prop(rpdat, 'lnx_dithering_strength') + layout.separator() col = layout.column() diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index 8c752ff..aa07825 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -660,6 +660,12 @@ const float waterDensity = """ + str(round(rpdat.lnx_water_density * 100) / 100) const float waterRefract = """ + str(round(rpdat.lnx_water_refract * 100) / 100) + """; const float waterReflect = """ + str(round(rpdat.lnx_water_reflect * 100) / 100) + """; """) + + if '_CDitheringStrength' in defs: + f.write( + f'const float ditherStrengthValue = {rpdat.lnx_dithering_strength};\n' + ) + if rpdat.rp_ssgi == 'SSAO' or rpdat.rp_ssgi == 'RTAO' or rpdat.rp_volumetriclight: f.write( """const float ssaoRadius = """ + str(round(rpdat.lnx_ssgi_radius * 100) / 100) + """;