Skip to content

Commit

Permalink
Add HSL Adjustments to the effects module.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 476144167
  • Loading branch information
leonwind authored and marcbaechinger committed Oct 19, 2022
1 parent afd829e commit 2c27cd8
Show file tree
Hide file tree
Showing 11 changed files with 650 additions and 0 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#version 100
// Copyright 2022 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// ES 2 fragment shader that samples from a (non-external) texture with
// uTexSampler. It then converts the RGB color input into HSL and adjusts
// the Hue, Saturation, and Lightness and converts it then back to RGB.

// We use the algorithm based on the work by Sam Hocevar, which optimizes
// for an efficient branchless RGB <-> HSL conversion. A blog post is
// at https://www.chilliant.com/rgb2hsv.html and it is further explained at
// http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv.

precision highp float;
uniform sampler2D uTexSampler;
// uHueAdjustmentDegrees, uSaturationAdjustment, and uLightnessAdjustment
// are normalized to the unit interval [0, 1].
uniform float uHueAdjustmentDegrees;
uniform float uSaturationAdjustment;
uniform float uLightnessAdjustment;
varying vec2 vTexSamplingCoord;

const float epsilon = 1e-10;

vec3 rgbToHcv(vec3 rgb) {
vec4 p = (rgb.g < rgb.b)
? vec4(rgb.bg, -1.0, 2.0 / 3.0)
: vec4(rgb.gb, 0.0, -1.0 / 3.0);
vec4 q = (rgb.r < p.x)
? vec4(p.xyw, rgb.r)
: vec4(rgb.r, p.yzx);
float c = q.x - min(q.w, q.y);
float h = abs((q.w - q.y) / (6.0 * c + epsilon) + q.z);
return vec3(h, c, q.x);
}

vec3 rgbToHsl(vec3 rgb) {
vec3 hcv = rgbToHcv(rgb);
float l = hcv.z - hcv.y * 0.5;
float s = hcv.y / (1.0 - abs(l * 2.0 - 1.0) + epsilon);
return vec3(hcv.x, s, l);
}

vec3 hueToRgb(float hue) {
float r = abs(hue * 6.0 - 3.0) - 1.0;
float g = 2.0 - abs(hue * 6.0 - 2.0);
float b = 2.0 - abs(hue * 6.0 - 4.0);
return clamp(vec3(r, g, b), 0.0, 1.0);
}

vec3 hslToRgb(vec3 hsl) {
vec3 rgb = hueToRgb(hsl.x);
float c = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
return (rgb - 0.5) * c + hsl.z;
}

void main() {
vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
vec3 hslColor = rgbToHsl(inputColor.rgb);

hslColor.x = mod(hslColor.x + uHueAdjustmentDegrees, 1.0);
hslColor.y = clamp(hslColor.y + uSaturationAdjustment, 0.0, 1.0);
hslColor.z = clamp(hslColor.z + uLightnessAdjustment, 0.0, 1.0);

gl_FragColor = vec4(hslToRgb(hslColor), inputColor.a);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.media3.effect;

import static com.google.android.exoplayer2.util.Assertions.checkArgument;

import android.content.Context;
import androidx.media3.common.FrameProcessingException;
import com.google.errorprone.annotations.CanIgnoreReturnValue;

/** Adjusts the HSL (Hue, Saturation, and Lightness) of a frame. */
public class HslAdjustment implements GlEffect {

/** A builder for {@code HslAdjustment} instances. */
public static final class Builder {
private float hueAdjustment;
private float saturationAdjustment;
private float lightnessAdjustment;

/** Creates a new instance with the default values. */
public Builder() {}

/**
* Rotates the hue of the frame by {@code hueAdjustmentDegrees}.
*
* <p>The Hue of the frame is defined in the interval of [0, 360] degrees. The actual degrees of
* hue adjustment applied is {@code hueAdjustmentDegrees % 360}.
*
* @param hueAdjustmentDegrees The hue adjustment in rotation degrees. The default value is
* {@code 0}, which means no change is applied.
*/
@CanIgnoreReturnValue
public Builder adjustHue(float hueAdjustmentDegrees) {
hueAdjustment = hueAdjustmentDegrees % 360;
return this;
}

/**
* Adjusts the saturation of the frame by {@code saturationAdjustment}.
*
* <p>Saturation is defined in the interval of [0, 100] where a saturation of {@code 0} will
* generate a grayscale frame and a saturation of {@code 100} has a maximum separation between
* the colors.
*
* @param saturationAdjustment The difference of how much the saturation will be adjusted in
* either direction. Needs to be in the interval of [-100, 100] and the default value is
* {@code 0}, which means no change is applied.
*/
@CanIgnoreReturnValue
public Builder adjustSaturation(float saturationAdjustment) {
checkArgument(
-100 <= saturationAdjustment && saturationAdjustment <= 100,
"Can adjust the saturation by only 100 in either direction, but provided "
+ saturationAdjustment);
this.saturationAdjustment = saturationAdjustment;
return this;
}

/**
* Adjusts the lightness of the frame by {@code lightnessAdjustment}.
*
* <p>Lightness is defined in the interval of [0, 100] where a lightness of {@code 0} is a black
* frame and a lightness of {@code 100} is a white frame.
*
* @param lightnessAdjustment The difference by how much the lightness will be adjusted in
* either direction. Needs to be in the interval of [-100, 100] and the default value is
* {@code 0}, which means no change is applied.
*/
@CanIgnoreReturnValue
public Builder adjustLightness(float lightnessAdjustment) {
checkArgument(
-100 <= lightnessAdjustment && lightnessAdjustment <= 100,
"Can adjust the lightness by only 100 in either direction, but provided "
+ lightnessAdjustment);
this.lightnessAdjustment = lightnessAdjustment;
return this;
}

/** Creates a new {@link HslAdjustment} instance. */
public HslAdjustment build() {
return new HslAdjustment(hueAdjustment, saturationAdjustment, lightnessAdjustment);
}
}

/** Indicates the hue adjustment in degrees. */
public final float hueAdjustmentDegrees;
/** Indicates the saturation adjustment. */
public final float saturationAdjustment;
/** Indicates the lightness adjustment. */
public final float lightnessAdjustment;

private HslAdjustment(
float hueAdjustmentDegrees, float saturationAdjustment, float lightnessAdjustment) {
this.hueAdjustmentDegrees = hueAdjustmentDegrees;
this.saturationAdjustment = saturationAdjustment;
this.lightnessAdjustment = lightnessAdjustment;
}

@Override
public HslProcessor toGlTextureProcessor(Context context, boolean useHdr)
throws FrameProcessingException {
return new HslProcessor(context, /* hslAdjustment= */ this, useHdr);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.media3.effect;

import static com.google.android.exoplayer2.util.Assertions.checkArgument;

import android.content.Context;
import android.opengl.GLES20;
import android.opengl.Matrix;
import android.util.Pair;
import androidx.media3.common.FrameProcessingException;
import com.google.android.exoplayer2.util.GlProgram;
import com.google.android.exoplayer2.util.GlUtil;
import java.io.IOException;

/** Applies the {@link HslAdjustment} to each frame in the fragment shader. */
/* package */ final class HslProcessor extends SingleFrameGlTextureProcessor {
private static final String VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_hsl_es2.glsl";

private final GlProgram glProgram;

/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param hslAdjustment The {@link HslAdjustment} to apply to each frame in order.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in gamma RGB BT.709.
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/
public HslProcessor(Context context, HslAdjustment hslAdjustment, boolean useHdr)
throws FrameProcessingException {
super(useHdr);
// TODO(b/241241680): Check if HDR <-> HSL works the same or not.
checkArgument(!useHdr, "HDR is not yet supported.");

try {
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (IOException | GlUtil.GlException e) {
throw new FrameProcessingException(e);
}

// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);

float[] identityMatrix = new float[16];
Matrix.setIdentityM(identityMatrix, /* smOffset= */ 0);
glProgram.setFloatsUniform("uTransformationMatrix", identityMatrix);
glProgram.setFloatsUniform("uTexTransformationMatrix", identityMatrix);

// OpenGL operates in a [0, 1] unit range and thus we transform the HSL intervals into
// the unit interval as well. The hue is defined in the [0, 360] interval and saturation
// and lightness in the [0, 100] interval.
glProgram.setFloatUniform("uHueAdjustmentDegrees", hslAdjustment.hueAdjustmentDegrees / 360);
glProgram.setFloatUniform("uSaturationAdjustment", hslAdjustment.saturationAdjustment / 100);
glProgram.setFloatUniform("uLightnessAdjustment", hslAdjustment.lightnessAdjustment / 100);
}

@Override
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
return Pair.create(inputWidth, inputHeight);
}

@Override
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
glProgram.bindAttributesAndUniforms();

// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e, presentationTimeUs);
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 2c27cd8

Please sign in to comment.