namespace UnityEngine.Rendering { // Has to be kept in sync with PhysicalCamera.hlsl /// /// A set of color manipulation utilities. /// public static class ColorUtils { /// /// Calibration constant (K) used for our virtual reflected light meter. Modifying this will lead to a change on how average scene luminance /// gets mapped to exposure. /// static public float s_LightMeterCalibrationConstant = 12.5f; /// /// Factor used for our lens system w.r.t. exposure calculation. Modifying this will lead to a change on how linear exposure /// multipliers are computed from EV100 values (and viceversa). s_LensAttenuation models transmission attenuation and lens vignetting. /// Note that according to the standard ISO 12232, a lens saturates at s_LensAttenuation = 0.78f (under ISO 100). /// static public float s_LensAttenuation = 0.65f; /// /// Scale applied to exposure caused by lens imperfection. It is computed from s_LensAttenuation as follow: /// (78 / ( S * q )) where S = 100 and q = s_LensAttenuation /// static public float lensImperfectionExposureScale { get => (78.0f / (100.0f * s_LensAttenuation)); } /// /// An analytical model of chromaticity of the standard illuminant, by Judd et al. /// http://en.wikipedia.org/wiki/Standard_illuminant#Illuminant_series_D /// Slightly modifed to adjust it with the D65 white point (x=0.31271, y=0.32902). /// /// /// public static float StandardIlluminantY(float x) => 2.87f * x - 3f * x * x - 0.27509507f; /// /// CIE xy chromaticity to CAT02 LMS. /// http://en.wikipedia.org/wiki/LMS_color_space#CAT02 /// /// /// /// public static Vector3 CIExyToLMS(float x, float y) { float Y = 1f; float X = Y * x / y; float Z = Y * (1f - x - y) / y; float L = 0.7328f * X + 0.4296f * Y - 0.1624f * Z; float M = -0.7036f * X + 1.6975f * Y + 0.0061f * Z; float S = 0.0030f * X + 0.0136f * Y + 0.9834f * Z; return new Vector3(L, M, S); } /// /// Converts white balancing parameter to LMS coefficients. /// /// A temperature offset, in range [-100;100]. /// A tint offset, in range [-100;100]. /// LMS coefficients. public static Vector3 ColorBalanceToLMSCoeffs(float temperature, float tint) { // Range ~[-1.5;1.5] works best float t1 = temperature / 65f; float t2 = tint / 65f; // Get the CIE xy chromaticity of the reference white point. // Note: 0.31271 = x value on the D65 white point float x = 0.31271f - t1 * (t1 < 0f ? 0.1f : 0.05f); float y = StandardIlluminantY(x) + t2 * 0.05f; // Calculate the coefficients in the LMS space. var w1 = new Vector3(0.949237f, 1.03542f, 1.08728f); // D65 white point var w2 = CIExyToLMS(x, y); return new Vector3(w1.x / w2.x, w1.y / w2.y, w1.z / w2.z); } /// /// Pre-filters shadows, midtones and highlights trackball values for shader use. /// /// A color used for shadows. /// A color used for midtones. /// A color used for highlights. /// The three input colors pre-filtered for shader use. public static (Vector4, Vector4, Vector4) PrepareShadowsMidtonesHighlights(in Vector4 inShadows, in Vector4 inMidtones, in Vector4 inHighlights) { float weight; var shadows = inShadows; shadows.x = Mathf.GammaToLinearSpace(shadows.x); shadows.y = Mathf.GammaToLinearSpace(shadows.y); shadows.z = Mathf.GammaToLinearSpace(shadows.z); weight = shadows.w * (Mathf.Sign(shadows.w) < 0f ? 1f : 4f); shadows.x = Mathf.Max(shadows.x + weight, 0f); shadows.y = Mathf.Max(shadows.y + weight, 0f); shadows.z = Mathf.Max(shadows.z + weight, 0f); shadows.w = 0f; var midtones = inMidtones; midtones.x = Mathf.GammaToLinearSpace(midtones.x); midtones.y = Mathf.GammaToLinearSpace(midtones.y); midtones.z = Mathf.GammaToLinearSpace(midtones.z); weight = midtones.w * (Mathf.Sign(midtones.w) < 0f ? 1f : 4f); midtones.x = Mathf.Max(midtones.x + weight, 0f); midtones.y = Mathf.Max(midtones.y + weight, 0f); midtones.z = Mathf.Max(midtones.z + weight, 0f); midtones.w = 0f; var highlights = inHighlights; highlights.x = Mathf.GammaToLinearSpace(highlights.x); highlights.y = Mathf.GammaToLinearSpace(highlights.y); highlights.z = Mathf.GammaToLinearSpace(highlights.z); weight = highlights.w * (Mathf.Sign(highlights.w) < 0f ? 1f : 4f); highlights.x = Mathf.Max(highlights.x + weight, 0f); highlights.y = Mathf.Max(highlights.y + weight, 0f); highlights.z = Mathf.Max(highlights.z + weight, 0f); highlights.w = 0f; return (shadows, midtones, highlights); } /// /// Pre-filters lift, gamma and gain trackball values for shader use. /// /// A color used for lift. /// A color used for gamma. /// A color used for gain. /// The three input colors pre-filtered for shader use. public static (Vector4, Vector4, Vector4) PrepareLiftGammaGain(in Vector4 inLift, in Vector4 inGamma, in Vector4 inGain) { var lift = inLift; lift.x = Mathf.GammaToLinearSpace(lift.x) * 0.15f; lift.y = Mathf.GammaToLinearSpace(lift.y) * 0.15f; lift.z = Mathf.GammaToLinearSpace(lift.z) * 0.15f; float lumLift = Luminance(lift); lift.x = lift.x - lumLift + lift.w; lift.y = lift.y - lumLift + lift.w; lift.z = lift.z - lumLift + lift.w; lift.w = 0f; var gamma = inGamma; gamma.x = Mathf.GammaToLinearSpace(gamma.x) * 0.8f; gamma.y = Mathf.GammaToLinearSpace(gamma.y) * 0.8f; gamma.z = Mathf.GammaToLinearSpace(gamma.z) * 0.8f; float lumGamma = Luminance(gamma); gamma.w += 1f; gamma.x = 1f / Mathf.Max(gamma.x - lumGamma + gamma.w, 1e-03f); gamma.y = 1f / Mathf.Max(gamma.y - lumGamma + gamma.w, 1e-03f); gamma.z = 1f / Mathf.Max(gamma.z - lumGamma + gamma.w, 1e-03f); gamma.w = 0f; var gain = inGain; gain.x = Mathf.GammaToLinearSpace(gain.x) * 0.8f; gain.y = Mathf.GammaToLinearSpace(gain.y) * 0.8f; gain.z = Mathf.GammaToLinearSpace(gain.z) * 0.8f; float lumGain = Luminance(gain); gain.w += 1f; gain.x = gain.x - lumGain + gain.w; gain.y = gain.y - lumGain + gain.w; gain.z = gain.z - lumGain + gain.w; gain.w = 0f; return (lift, gamma, gain); } /// /// Pre-filters colors used for the split toning effect. /// /// A color used for shadows. /// A color used for highlights. /// The balance between the shadow and highlight colors, in range [-100;100]. /// The two input colors pre-filtered for shader use. public static (Vector4, Vector4) PrepareSplitToning(in Vector4 inShadows, in Vector4 inHighlights, float balance) { // As counter-intuitive as it is, to make split-toning work the same way it does in // Adobe products we have to do all the maths in sRGB... So do not convert these to // linear before sending them to the shader, this isn't a bug! var shadows = inShadows; var highlights = inHighlights; // Balance is stored in `shadows.w` shadows.w = balance / 100f; highlights.w = 0f; return (shadows, highlights); } /// /// Returns the luminance of the specified color. The input is considered to be in linear /// space with sRGB primaries and a D65 white point. /// /// The color to compute the luminance for. /// A luminance value. public static float Luminance(in Color color) => color.r * 0.2126729f + color.g * 0.7151522f + color.b * 0.072175f; /// /// Computes an exposure value (EV100) from physical camera settings. /// /// The camera aperture. /// The camera exposure time. /// The camera sensor sensitivity. /// An exposure value, in EV100. public static float ComputeEV100(float aperture, float shutterSpeed, float ISO) { // References: // "Moving Frostbite to PBR" (Sébastien Lagarde & Charles de Rousiers) // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf // "Implementing a Physically Based Camera" (Padraic Hennessy) // https://placeholderart.wordpress.com/2014/11/16/implementing-a-physically-based-camera-understanding-exposure/ // EV number is defined as: // 2^ EV_s = N^2 / t and EV_s = EV_100 + log2 (S /100) // This gives // EV_s = log2 (N^2 / t) // EV_100 + log2 (S /100) = log2 (N^2 / t) // EV_100 = log2 (N^2 / t) - log2 (S /100) // EV_100 = log2 (N^2 / t . 100 / S) return Mathf.Log((aperture * aperture) / shutterSpeed * 100f / ISO, 2f); } /// /// Converts an exposure value (EV100) to a linear multiplier. /// /// The exposure value to convert, in EV100. /// A linear multiplier. public static float ConvertEV100ToExposure(float EV100) { // Compute the maximum luminance possible with H_sbs sensitivity // maxLum = 78 / ( S * q ) * N^2 / t // = 78 / ( S * q ) * 2^ EV_100 // = 78 / (100 * s_LensAttenuation) * 2^ EV_100 // = lensImperfectionExposureScale * 2^ EV // Reference: http://en.wikipedia.org/wiki/Film_speed float maxLuminance = lensImperfectionExposureScale * Mathf.Pow(2.0f, EV100); return 1.0f / maxLuminance; } /// /// Converts a linear multiplier to an exposure value (EV100). /// /// A linear multiplier. /// An exposure value, in EV100. public static float ConvertExposureToEV100(float exposure) { // Compute the maximum luminance possible with H_sbs sensitivity // EV_100 = log2( S * q / (78 * exposure) ) // = log2( 100 * s_LensAttenuation / (78 * exposure) ) // = log2( 1.0f / (lensImperfectionExposureScale * exposure) ) // Reference: http://en.wikipedia.org/wiki/Film_speed return Mathf.Log(1.0f / (lensImperfectionExposureScale * exposure), 2.0f); } /// /// Computes an exposure value (EV100) from an average luminance value. /// /// An average luminance value. /// An exposure value, in EV100. public static float ComputeEV100FromAvgLuminance(float avgLuminance) { // The middle grey used will be determined by the s_LightMeterCalibrationConstant. // The suggested (ISO 2720) range is 10.64 to 13.4. Common values used by // manufacturers range from 11.37 to 14. Ref: https://en.wikipedia.org/wiki/Light_meter // The default is 12.5% as it is the closest to 12.7% in order to have // a middle gray at 18% with a sqrt(2) room for specular highlights // Note that this gives equivalent results as using an incident light meter // with a calibration constant of C=314. float K = s_LightMeterCalibrationConstant; return Mathf.Log(avgLuminance * 100f / K, 2f); } /// /// Computes the required ISO to reach . /// /// The camera aperture. /// The camera exposure time. /// The target exposure value (EV100) to reach. /// The required sensor sensitivity (ISO). public static float ComputeISO(float aperture, float shutterSpeed, float targetEV100) => ((aperture * aperture) * 100f) / (shutterSpeed * Mathf.Pow(2f, targetEV100)); /// /// Converts a color value to its 32-bit hexadecimal representation. /// /// The color to convert. /// A 32-bit hexadecimal representation of the color. public static uint ToHex(Color c) => ((uint)(c.a * 255) << 24) | ((uint)(c.r * 255) << 16) | ((uint)(c.g * 255) << 8) | (uint)(c.b * 255); /// /// Converts a 32-bit hexadecimal value to a color value. /// /// A 32-bit hexadecimal value. /// A color value. public static Color ToRGBA(uint hex) => new Color(((hex >> 16) & 0xff) / 255f, ((hex >> 8) & 0xff) / 255f, (hex & 0xff) / 255f, ((hex >> 24) & 0xff) / 255f); } }