/// Assert that the two HSLs represent the same-looking color /// (as opposed to having exactly the same values) public static void AssertNearlyEqualHSL(HSLColor lhs, HSLColor rhs) { float eps = 1e-6f; float dist = (HslAsRectilinear(lhs) - HslAsRectilinear(rhs)).magnitude; if (!AlmostEqual(dist, 0, eps) || !AlmostEqual(lhs.a, rhs.a, eps)) { Assert.Fail("{0} !~ {1}", Repr(lhs), Repr(rhs)); } }
// Convert from polar to rectilinear coordinates for ease of comparison static Vector3 HslAsRectilinear(HSLColor hsl) { float radius = .5f - Mathf.Abs(hsl.l - .5f); radius = radius * hsl.s; float angle = hsl.HueDegrees * Mathf.Deg2Rad; return(new Vector3(radius * Mathf.Cos(angle), radius * Mathf.Sin(angle), hsl.l)); }
/// Assert that the two colors have the same field values /// and that orthogonal dimensions are preserved for some L and S edge cases. public static void AssertNearlyEqualHSLStrict(HSLColor lhs, HSLColor rhs) { float eps = 1e-4f; if (!HueAlmostEqual(lhs.h, rhs.h, eps) || !AlmostEqual(lhs.s, rhs.s, eps) || !AlmostEqual(lhs.l, rhs.l, eps) || !AlmostEqual(lhs.a, rhs.a, eps)) { Assert.Fail("{0} !~ {1}", Repr(lhs), Repr(rhs)); } }
// // Utility methods // protected static string Repr(HSLColor hsl) { return(string.Format("<HSLA {0} {1} {2} {3}>", hsl.h, hsl.s, hsl.l, hsl.a)); }
/// Unconditionally increments m_LeadingQuadIndex by 1 or 2 private void AppendLeadingQuad( bool bGenerateNew, float opacity01, Vector3 vCenter, Vector3 vForward, Vector3 vNormal, Vector3 vRight, MasterBrush rMasterBrush, out int earliestChangedQuad) { // Get the current stroke from the MasterBrush so that quad positions and // orientations can be calculated. Vector3[] aVerts = rMasterBrush.m_Vertices; Vector3[] aNorms = rMasterBrush.m_Normals; Color32[] aColors = rMasterBrush.m_Colors; int stride = Stride; // Lay leading quad int iVertIndex = m_LeadingQuadIndex * 6; PositionQuad(aVerts, iVertIndex, vCenter, vForward, vRight); for (int i = 0; i < 6; ++i) { aNorms[iVertIndex + i] = vNormal; } earliestChangedQuad = m_LeadingQuadIndex; Color32 cColor = m_Color; cColor.a = (byte)(opacity01 * 255.0f); Color32 cLastColor = (iVertIndex - stride >= 0) ? aColors[iVertIndex - stride + 4] : cColor; aColors[iVertIndex] = cLastColor; aColors[iVertIndex + 1] = cColor; aColors[iVertIndex + 2] = cLastColor; aColors[iVertIndex + 3] = cLastColor; aColors[iVertIndex + 4] = cColor; aColors[iVertIndex + 5] = cColor; ++m_LeadingQuadIndex; // Create duplicates if we have backfaces enabled. if (m_EnableBackfaces) { int iCurrVertIndex = m_LeadingQuadIndex * 6; CreateDuplicateQuad(aVerts, aNorms, m_LeadingQuadIndex, vNormal); Color32 backColor, lastBackColor; if (m_Desc.m_BackfaceHueShift == 0) { backColor = cColor; lastBackColor = cLastColor; } else { HSLColor hsl = (HSLColor)(Color)m_Color; hsl.HueDegrees += m_Desc.m_BackfaceHueShift; backColor = (Color32)(Color)hsl; lastBackColor = (iCurrVertIndex - stride >= 0) ? aColors[iCurrVertIndex - stride + 4] : backColor; } aColors[iCurrVertIndex] = lastBackColor; aColors[iCurrVertIndex + 1] = lastBackColor; aColors[iCurrVertIndex + 2] = backColor; aColors[iCurrVertIndex + 3] = lastBackColor; aColors[iCurrVertIndex + 4] = backColor; aColors[iCurrVertIndex + 5] = backColor; ++m_LeadingQuadIndex; } // Walk backward and smooth out previous quads. int iStripLength = m_LeadingQuadIndex; // In solids int iSegmentLength = m_LeadingQuadIndex - m_InitialQuadIndex; // In solids if (m_EnableBackfaces) { iStripLength /= 2; iSegmentLength /= 2; } // We don't need to smooth anything if our strip is only 1 quad. if (iStripLength > 1) { // Indexes for later use int iIndexingOffset = m_EnableBackfaces ? 2 : 1; int iBackQuadIndex = m_LeadingQuadIndex - (3 * iIndexingOffset); int iBackQuadVert = iBackQuadIndex * 6; int iMidQuadIndex = m_LeadingQuadIndex - (2 * iIndexingOffset); int iMidQuadVert = iMidQuadIndex * 6; int iFrontQuadIndex = m_LeadingQuadIndex - iIndexingOffset; int iFrontQuadVert = iFrontQuadIndex * 6; if (iSegmentLength == 1) { // If we've got a long strip, but this segment is only 1 quad, touch up the previous quad. PositionQuad(aVerts, iMidQuadVert, m_LastQuadCenter, m_LastQuadForward, m_LastQuadRight); earliestChangedQuad = Mathf.Min(earliestChangedQuad, iMidQuadIndex); // Fuse back to mid if it exists and if they used to be fused. if (iStripLength > 2 && m_LastSegmentLengthSolids > 1) { FuseQuads(aVerts, aNorms, iBackQuadVert, iMidQuadVert, bGenerateNew); if (m_EnableBackfaces) { MakeConsistentBacksideQuad(aVerts, aNorms, iBackQuadVert); } } else if (bGenerateNew && m_LastSegmentLengthSolids == 1) { // If we've got a strip longer than one quad, and this segment is only one quad, it // means this is the start of a new segment. If we're beginning a new segment and // our previous segment is only one quad, squash that quad to clean up artifacts. PositionQuad(aVerts, iMidQuadVert, m_LastQuadCenter, Vector3.zero, Vector3.zero); } if (m_EnableBackfaces) { MakeConsistentBacksideQuad(aVerts, aNorms, iMidQuadVert); } } else if (iSegmentLength == 2) { // If we've got a long strip, but this segment is only 2 quads, just fuse. FuseQuads(aVerts, aNorms, iMidQuadVert, iFrontQuadVert, bGenerateNew); if (m_EnableBackfaces) { MakeConsistentBacksideQuad(aVerts, aNorms, iMidQuadVert); MakeConsistentBacksideQuad(aVerts, aNorms, iFrontQuadVert); } } else { // Set mid quad to the midpoint of back and front quads. for (int i = 0; i < 6; ++i) { aVerts[iMidQuadVert + i] = (aVerts[iBackQuadVert + i] + aVerts[iFrontQuadVert + i]) * 0.5f; } // Patch up the holes by connecting the leading edge of the back quad to the trailing // edge of the mid, and do the same from mid to front. FuseQuads(aVerts, aNorms, iBackQuadVert, iMidQuadVert, bGenerateNew); FuseQuads(aVerts, aNorms, iMidQuadVert, iFrontQuadVert, bGenerateNew); if (m_EnableBackfaces) { MakeConsistentBacksideQuad(aVerts, aNorms, iBackQuadVert); MakeConsistentBacksideQuad(aVerts, aNorms, iMidQuadVert); MakeConsistentBacksideQuad(aVerts, aNorms, iFrontQuadVert); } // Make sure the UVs are proper UpdateUVsForQuad(iMidQuadIndex); } } }
/// Returns true on success, false on failure. /// Failure cases are guaranteed to be identical to the failure cases of RawValueToColor /// Use if converting raw -> Color is lossy; otherwise, use RawValueToColor /// HDR color pickers might disallow conversion to HSL. static bool RawValueToHSLColor(ColorPickerMode mode, Vector3 raw, out HSLColor color) { const float EPSILON = 1e-5f; switch (mode) { case ColorPickerMode.SV_H_Rect: color = HSLColor.FromHSV(raw.z * HSLColor.HUE_MAX, raw.x, raw.y); return(true); case ColorPickerMode.HS_L_Polar: { var position = new Vector2(raw.x - 0.5f, raw.y - 0.5f) * 2; var radius = position.magnitude; color = new HSLColor(); if (radius > 1) { if (radius > 1 + EPSILON) { return(false); } else { radius = 1; } } // x direction is 0 degrees (red) color.HueDegrees = Mathf.Atan2(position.y, position.x) * Mathf.Rad2Deg; color.s = radius; color.l = raw.z; color.a = 1; return(true); } case ColorPickerMode.SL_H_Triangle: { color.h = 0; // assigned later color.l = raw.y; float maxChroma = SQRT3 * ((color.l < .5f) ? color.l : (1 - color.l)); color.s = (maxChroma == 0) ? 0 : raw.x / maxChroma; color.a = 1; color.Hue01 = raw.z; return(0 <= raw.x && raw.x <= maxChroma); } case ColorPickerMode.HL_S_Polar: { var position = new Vector2(raw.x - 0.5f, raw.y - 0.5f) * 2; var radius = position.magnitude; color = new HSLColor(); if (radius > 1) { if (radius > 1 + EPSILON) { return(false); } else { radius = 1; } } color.HueDegrees = Mathf.Atan2(position.y, position.x) * Mathf.Rad2Deg; color.l = 1 - radius; color.s = 1 - raw.z; color.a = 1; return(true); } case ColorPickerMode.HS_LogV_Polar: throw new InvalidOperationException("This is a HDR mode"); default: Debug.Assert(false); color = new HSLColor(); return(false); } }
/// Can never fail (unlike RawValueToColor) /// /// Note: Behavior is currently undefined if an hdr color is passed in, /// but the color picker cannot handle hdr. public static Vector3 ColorToRawValue(ColorPickerMode mode, Color rgb) { bool colorIsHdr = (rgb.r > 1 || rgb.g > 1 || rgb.b > 1); // If we do this a lot, maybe add this to ColorPickerInfo.hdr? // Or better, refactor this whole mess of code into small mode-specific classes. bool pickerSupportsHdr = (mode == ColorPickerMode.HS_LogV_Polar); if (colorIsHdr && !pickerSupportsHdr) { // Shouldn't happen except in experimental Debug.LogErrorFormat("Truncating HDR color to LDR"); float h, s, v; Color.RGBToHSV(rgb, out h, out s, out v); rgb = Color.HSVToRGB(h, s, v, hdr: false); } switch (mode) { case ColorPickerMode.SV_H_Rect: { float h, s, v; Color.RGBToHSV(rgb, out h, out s, out v); return(new Vector3(s, v, h)); } case ColorPickerMode.HS_L_Polar: { // H is angle, S is radius, L is depth HSLColor color = (HSLColor)rgb; var angle = color.HueDegrees * Mathf.Deg2Rad; var vector = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * color.s / 2 + new Vector2(0.5f, 0.5f); return(new Vector3(vector.x, vector.y, color.l)); } case ColorPickerMode.HL_S_Polar: { // H is angle, (1-L) is radius, (1-S) is depth HSLColor color = (HSLColor)rgb; var angle = color.HueDegrees * Mathf.Deg2Rad; float radius = 1 - color.l; var vector = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius / 2 + new Vector2(0.5f, 0.5f); return(new Vector3(vector.x, vector.y, 1 - color.s)); } case ColorPickerMode.SL_H_Triangle: { HSLColor color = (HSLColor)rgb; Vector3 ret = new Vector3(); ret.y = color.l; ret.z = color.Hue01; float maxChroma = SQRT3 * ((color.l < .5f) ? color.l : (1 - color.l)); ret.x = maxChroma * color.s; return(ret); } case ColorPickerMode.HS_LogV_Polar: { // H is angle, S is radius, log(V) is depth // This only needs to be > 0 and < the minimum ColorPickerMode.HS_LogV_Polar float kMinValue = 1e-7f; float h, s, v; Color.RGBToHSV(rgb, out h, out s, out v); Vector2 cartesian; { float angle = h * (Mathf.PI * 2); cartesian = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * s; // convert from [-1, 1] to [0, 1] cartesian = cartesian / 2 + new Vector2(.5f, .5f); } // Log-remap [2^-n, 2^n] to [-n, n] v += MinHDRValue; float slider = Mathf.Log(Mathf.Max(v, kMinValue), 2); slider = Mathf.Clamp(slider, kLogVMin, sm_LogVMax); // remap from [min, max] to [0, 1] slider = (slider - kLogVMin) / (sm_LogVMax - kLogVMin); return(new Vector3(cartesian.x, cartesian.y, slider)); } default: return(new Vector3(1, 1, 1)); } }