From dd79b3c0d5e894487e6c067698174c6809fecf58 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 8 Jun 2021 13:14:48 +0400 Subject: [PATCH] Add noise to blur to remove color banding. --- .../group/calls_group_viewport_opengl.cpp | 252 ++++++++++++++++-- .../calls/group/calls_group_viewport_opengl.h | 6 +- Telegram/lib_ui | 2 +- 3 files changed, 234 insertions(+), 26 deletions(-) diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp index 1eeda2a1ff..fae2729abf 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -24,8 +24,12 @@ using namespace Ui::GL; constexpr auto kScaleForBlurTextureIndex = 3; constexpr auto kFirstBlurPassTextureIndex = 4; -constexpr auto kBlurTextureSizeFactor = 1.7; -constexpr auto kBlurOpacity = 0.7; +constexpr auto kNoiseTextureSize = 256; + +// The more the scale - more blurred the image. +constexpr auto kBlurTextureSizeFactor = 4.; +constexpr auto kBlurOpacity = 0.65; +constexpr auto kDitherNoiseAmount = 0.002; constexpr auto kMinCameraVisiblePart = 0.75; constexpr auto kQuads = 7; @@ -66,15 +70,128 @@ const int diameter = 2 * radius + 1; }; } +[[nodiscard]] ShaderPart FragmentGenerateNoise() { + const auto size = QString::number(kNoiseTextureSize); + return { + .header = R"( +const float permTexUnit = 1.0 / )" + size + R"(.0; +const float permTexUnitHalf = 0.5 / )" + size + R"(.0; +const float grainsize = 1.3; +const float noiseCoordRotation = 1.425; +const vec2 dimensions = vec2()" + size + ", " + size + R"(); + +vec4 rnm(vec2 tc) { + float noise = sin(dot(tc, vec2(12.9898, 78.233))) * 43758.5453; + return vec4( + fract(noise), + fract(noise * 1.2154), + fract(noise * 1.3453), + fract(noise * 1.3647) + ) * 2.0 - 1.0; +} + +float fade(float t) { + return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); +} + +float pnoise3D(vec3 p) { + vec3 pi = permTexUnit * floor(p) + permTexUnitHalf; + vec3 pf = fract(p); + float perm = rnm(pi.xy).a; + float n000 = dot(rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, pf); + float n001 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(0.0, 0.0, 1.0)); + perm = rnm(pi.xy + vec2(0.0, permTexUnit)).a; + float n010 = dot( + rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, + pf - vec3(0.0, 1.0, 0.0)); + float n011 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(0.0, 1.0, 1.0)); + perm = rnm(pi.xy + vec2(permTexUnit, 0.0)).a; + float n100 = dot( + rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 0.0, 0.0)); + float n101 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 0.0, 1.0)); + perm = rnm(pi.xy + vec2(permTexUnit, permTexUnit)).a; + float n110 = dot( + rnm(vec2(perm, pi.z)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 1.0, 0.0)); + float n111 = dot( + rnm(vec2(perm, pi.z + permTexUnit)).rgb * 4.0 - 1.0, + pf - vec3(1.0, 1.0, 1.0)); + vec4 n_x = mix( + vec4(n000, n001, n010, n011), + vec4(n100, n101, n110, n111), + fade(pf.x)); + vec2 n_xy = mix(n_x.xy, n_x.zw, fade(pf.y)); + return mix(n_xy.x, n_xy.y, fade(pf.z)); +} + +vec2 rotateTexCoords(in lowp vec2 tc, in lowp float angle) { + float cosa = cos(angle); + float sina = sin(angle); + return vec2( + ((tc.x * 2.0 - 1.0) * cosa - (tc.y * 2.0 - 1.0) * sina) * 0.5 + 0.5, + ((tc.y * 2.0 - 1.0) * cosa + (tc.x * 2.0 - 1.0) * sina) * 0.5 + 0.5); +} +)", + .body = R"( + vec2 rotatedCoords = rotateTexCoords( + gl_FragCoord.xy / dimensions.xy, + noiseCoordRotation); + float intensity = pnoise3D(vec3( + rotatedCoords.x * dimensions.x / grainsize, + rotatedCoords.y * dimensions.y / grainsize, + 0.0)); + + // Looks like intensity is almost always in [-2, 2] range. + float clamped = clamp((intensity + 2.) * 0.25, 0., 1.); + result = vec4(clamped, 0., 0., 1.); +)", + }; +} + +[[nodiscard]] ShaderPart FragmentDitherNoise() { + const auto size = QString::number(kNoiseTextureSize); + return { + .header = R"( +uniform sampler2D n_texture; +)", + .body = R"( + vec2 noiseTextureCoord = gl_FragCoord.xy / )" + size + R"(.; + float noiseClamped = texture2D(n_texture, noiseTextureCoord).r; + float noiseIntensity = (noiseClamped * 4.) - 2.; + + vec3 lumcoeff = vec3(0.299, 0.587, 0.114); + float luminance = dot(result.rgb, lumcoeff); + float lum = smoothstep(0.2, 0.0, luminance) + luminance; + vec3 noiseColor = mix(vec3(noiseIntensity), vec3(0.0), pow(lum, 4.0)); + + result.rgb = result.rgb + noiseColor * noiseGrain; +)", + }; +} + // Depends on FragmentSampleTexture(). [[nodiscard]] ShaderPart FragmentFrameColor() { + const auto round = FragmentRoundCorners(); const auto blur = FragmentBlurTexture(true, 'b'); + const auto noise = FragmentDitherNoise(); return { .header = R"( uniform vec4 frameBg; uniform vec3 shadow; // fullHeight, shown, maxOpacity uniform float paused; // 0. <-> 1. + +)" + blur.header + round.header + noise.header + R"( + const float backgroundOpacity = )" + QString::number(kBlurOpacity) + R"(; +const float noiseGrain = )" + QString::number(kDitherNoiseAmount) + R"(; + float insideTexture() { vec2 textureHalf = vec2(0.5, 0.5); vec2 fromTextureCenter = abs(v_texcoord - textureHalf); @@ -82,10 +199,12 @@ float insideTexture() { float outsideCheck = dot(fromTextureEdge, fromTextureEdge); return step(outsideCheck, 0); } -)" + blur.header + R"( + vec4 background() { vec4 result; -)" + blur.body + R"( + +)" + blur.body + noise.body + R"( + return result; } )", @@ -99,7 +218,7 @@ vec4 background() { float shadowValue = max(1. - (shadowCoord / shadow.x), 0.); float shadowShown = max(shadowValue * shadow.y, paused) * shadow.z; result = vec4(result.rgb * (1. - shadowShown), result.a); -)", +)" + round.body, }; } @@ -119,10 +238,10 @@ vec4 background() { [[nodiscard]] QSize CountBlurredSize( QSize unscaled, - QSize viewport, + QSize outer, float factor) { - factor *= kBlurTextureSizeFactor; // The more the scale - more blurred the image. - const auto area = viewport / int(std::round(factor * cScale() / 100)); + factor *= kBlurTextureSizeFactor; + const auto area = outer / int(std::round(factor * cScale() / 100)); const auto scaled = unscaled.scaled(area, Qt::KeepAspectRatio); return (scaled.width() > unscaled.width() || scaled.height() > unscaled.height()) @@ -208,6 +327,9 @@ void Viewport::RendererGL::init( _frameBuffer->bind(); _frameBuffer->allocate(kValues * sizeof(GLfloat)); _downscaleProgram.yuv420.emplace(); + const auto downscaleVertexSource = VertexShader({ + VertexPassTextureCoord(), + }); _downscaleVertexShader = LinkProgram( &*_downscaleProgram.yuv420, VertexShader({ @@ -216,6 +338,9 @@ void Viewport::RendererGL::init( FragmentShader({ FragmentSampleYUV420Texture(), })).vertex; + if (!_downscaleProgram.yuv420->isLinked()) { + //... + } _blurProgram.emplace(); LinkProgram( &*_blurProgram, @@ -234,7 +359,6 @@ void Viewport::RendererGL::init( FragmentShader({ FragmentSampleYUV420Texture(), FragmentFrameColor(), - FragmentRoundCorners(), })).vertex; _imageProgram.emplace(); @@ -248,6 +372,8 @@ void Viewport::RendererGL::init( FragmentSampleARGB32Texture(), })); + validateNoiseTexture(f, 0); + _background.init(f); } @@ -287,6 +413,8 @@ void Viewport::RendererGL::deinit( _blurProgram = std::nullopt; _frameProgram.argb32 = std::nullopt; _frameProgram.yuv420 = std::nullopt; + _noiseTexture.destroy(f); + _noiseFramebuffer.destroy(f); for (auto &data : _tileData) { data.textures.destroy(f); } @@ -314,9 +442,10 @@ void Viewport::RendererGL::paint( not_null widget, QOpenGLFunctions &f) { _factor = widget->devicePixelRatio(); + const auto defaultFramebufferObject = widget->defaultFramebufferObject(); + validateDatas(); fillBackground(f); - const auto defaultFramebufferObject = widget->defaultFramebufferObject(); auto index = 0; for (const auto &tile : _owner->_tiles) { if (!tile->shown()) { @@ -584,7 +713,10 @@ void Viewport::RendererGL::paintTile( _frameBuffer->bind(); _frameBuffer->write(0, coords, sizeof(coords)); - const auto blurSize = CountBlurredSize(unscaled, _viewport, _factor); + const auto blurSize = CountBlurredSize( + unscaled, + geometry.size(), + _factor); prepareObjects(f, tileData, blurSize); f.glViewport(0, 0, blurSize.width(), blurSize.height()); @@ -625,10 +757,11 @@ void Viewport::RendererGL::paintTile( program->setUniformValue("paused", GLfloat(paused)); f.glActiveTexture(_rgbaFrame ? GL_TEXTURE1 : GL_TEXTURE3); - tileData.textures.bind( - f, - tileData.textureIndex * 5 + kFirstBlurPassTextureIndex); + tileData.textures.bind(f, kFirstBlurPassTextureIndex); program->setUniformValue("b_texture", GLint(_rgbaFrame ? 1 : 3)); + f.glActiveTexture(_rgbaFrame ? GL_TEXTURE2 : GL_TEXTURE5); + _noiseTexture.bind(f, 0); + program->setUniformValue("n_texture", GLint(_rgbaFrame ? 2 : 5)); program->setUniformValue( "texelOffset", GLfloat(1.f / blurSize.height())); @@ -692,7 +825,15 @@ void Viewport::RendererGL::prepareObjects( QOpenGLFunctions &f, TileData &tileData, QSize blurSize) { - tileData.textures.ensureCreated(f); + if (!tileData.textures.created()) { + tileData.textures.ensureCreated(f); // All are GL_LINEAR, except.. + tileData.textures.bind(f, kScaleForBlurTextureIndex); + + // kScaleForBlurTextureIndex is attached to framebuffer 0, + // and is used to draw to framebuffer 1 of the same size. + f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } tileData.framebuffers.ensureCreated(f); if (tileData.textureBlurSize == blurSize) { @@ -701,8 +842,6 @@ void Viewport::RendererGL::prepareObjects( tileData.textureBlurSize = blurSize; const auto create = [&](int framebufferIndex, int index) { - index += tileData.textureIndex * 5; - tileData.textures.bind(f, index); f.glTexImage2D( GL_TEXTURE_2D, @@ -762,7 +901,7 @@ void Viewport::RendererGL::bindFrame( ensureARGB32Program(); f.glUseProgram(program.argb32->programId()); f.glActiveTexture(GL_TEXTURE0); - tileData.textures.bind(f, tileData.textureIndex * 5 + 0); + tileData.textures.bind(f, 0); if (upload) { const auto &image = _userpicFrame ? tileData.userpicFrame @@ -785,7 +924,7 @@ void Viewport::RendererGL::bindFrame( const auto yuv = data.yuv420; f.glUseProgram(program.yuv420->programId()); f.glActiveTexture(GL_TEXTURE0); - tileData.textures.bind(f, tileData.textureIndex * 5 + 0); + tileData.textures.bind(f, 0); if (upload) { f.glPixelStorei(GL_UNPACK_ALIGNMENT, 1); uploadTexture( @@ -800,7 +939,7 @@ void Viewport::RendererGL::bindFrame( tileData.rgbaSize = QSize(); } f.glActiveTexture(GL_TEXTURE1); - tileData.textures.bind(f, tileData.textureIndex * 5 + 1); + tileData.textures.bind(f, 1); if (upload) { uploadTexture( f, @@ -812,7 +951,7 @@ void Viewport::RendererGL::bindFrame( yuv->u.data); } f.glActiveTexture(GL_TEXTURE2); - tileData.textures.bind(f, tileData.textureIndex * 5 + 2); + tileData.textures.bind(f, 2); if (upload) { uploadTexture( f, @@ -886,9 +1025,7 @@ void Viewport::RendererGL::drawFirstBlurPass( f.glUseProgram(_blurProgram->programId()); f.glActiveTexture(GL_TEXTURE0); - tileData.textures.bind( - f, - tileData.textureIndex * 5 + kScaleForBlurTextureIndex); + tileData.textures.bind(f, kScaleForBlurTextureIndex); _blurProgram->setUniformValue("b_texture", GLint(0)); _blurProgram->setUniformValue( @@ -1145,6 +1282,73 @@ void Viewport::RendererGL::validateDatas() { _names.setImage(std::move(paintToImage)); } +void Viewport::RendererGL::validateNoiseTexture( + QOpenGLFunctions &f, + GLuint defaultFramebufferObject) { + if (_noiseTexture.created()) { + return; + } + _noiseTexture.ensureCreated(f, GL_NEAREST, GL_REPEAT); + _noiseTexture.bind(f, 0); + f.glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + kNoiseTextureSize, + kNoiseTextureSize, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + nullptr); + + _noiseFramebuffer.ensureCreated(f); + _noiseFramebuffer.bind(f, 0); + + f.glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + _noiseTexture.id(0), + 0); + + f.glViewport(0, 0, kNoiseTextureSize, kNoiseTextureSize); + + const GLfloat coords[] = { + -1, -1, + -1, 1, + 1, 1, + 1, -1, + }; + auto buffer = QOpenGLBuffer(); + buffer.setUsagePattern(QOpenGLBuffer::StaticDraw); + buffer.create(); + buffer.bind(); + buffer.allocate(coords, sizeof(coords)); + + auto program = QOpenGLShaderProgram(); + LinkProgram( + &program, + VertexShader({}), + FragmentShader({ FragmentGenerateNoise() })); + f.glUseProgram(program.programId()); + + GLint position = program.attributeLocation("position"); + f.glVertexAttribPointer( + position, + 2, + GL_FLOAT, + GL_FALSE, + 2 * sizeof(GLfloat), + nullptr); + f.glEnableVertexAttribArray(position); + + f.glDrawArrays(GL_TRIANGLE_FAN, 0, 4); + + f.glDisableVertexAttribArray(position); + + f.glUseProgram(0); +} + void Viewport::RendererGL::validateOutlineAnimation( not_null tile, TileData &data) { diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h index 346c62c555..7b93247e04 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.h @@ -57,7 +57,6 @@ private: QImage userpicFrame; QRect nameRect; int nameVersion = 0; - mutable int textureIndex = 0; mutable int trackIndex = -1; mutable QSize rgbaSize; mutable QSize textureSize; @@ -102,6 +101,9 @@ private: TileData &tileData, QSize blurSize); void validateDatas(); + void validateNoiseTexture( + QOpenGLFunctions &f, + GLuint defaultFramebufferObject); void validateOutlineAnimation( not_null tile, TileData &data); @@ -142,6 +144,8 @@ private: std::optional _blurProgram; Program _frameProgram; std::optional _imageProgram; + Ui::GL::Textures<1> _noiseTexture; + Ui::GL::Framebuffers<1> _noiseFramebuffer; QOpenGLShader *_downscaleVertexShader = nullptr; QOpenGLShader *_frameVertexShader = nullptr; diff --git a/Telegram/lib_ui b/Telegram/lib_ui index 35236988b7..4ae6d31963 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit 35236988b7bc489b7683019db89636d4030db1ce +Subproject commit 4ae6d319634be7aa3530ad3e934a187f630bd6fd