using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text.RegularExpressions; using UnityEngine; namespace UnityEditor.Rendering { /// /// Class to Parse IES File /// [System.Serializable] public class IESReader { string m_FileFormatVersion; /// /// Version of the IES File /// public string FileFormatVersion { get { return m_FileFormatVersion; } } float m_TotalLumens; /// /// Total light intensity (in Lumens) stored on the file, usage of it is optional (through the prefab subasset inside the IESObject) /// public float TotalLumens { get { return m_TotalLumens; } } float m_MaxCandelas; /// /// Maximum of Candela in the IES File /// public float MaxCandelas { get { return m_MaxCandelas; } } int m_PhotometricType; /// /// Type of Photometric light in the IES file, varying per IES-Type and version /// public int PhotometricType { get { return m_PhotometricType; } } Dictionary m_KeywordDictionary = new Dictionary(); int m_VerticalAngleCount; int m_HorizontalAngleCount; float[] m_VerticalAngles; float[] m_HorizontalAngles; float[] m_CandelaValues; float m_MinDeltaVerticalAngle; float m_MinDeltaHorizontalAngle; float m_FirstHorizontalAngle; float m_LastHorizontalAngle; // File format references: // https://www.ies.org/product/standard-file-format-for-electronic-transfer-of-photometric-data/ // http://lumen.iee.put.poznan.pl/kw/iesna.txt // https://seblagarde.wordpress.com/2014/11/05/ies-light-format-specification-and-reader/ /// /// Main function to read the file /// /// The path to the IES File on disk. /// Return the error during the import otherwise null if no error public string ReadFile(string iesFilePath) { using (var iesReader = File.OpenText(iesFilePath)) { string versionLine = iesReader.ReadLine(); if (versionLine == null) { return "Premature end of file (empty file)."; } switch (versionLine.Trim()) { case "IESNA91": m_FileFormatVersion = "LM-63-1991"; break; case "IESNA:LM-63-1995": m_FileFormatVersion = "LM-63-1995"; break; case "IESNA:LM-63-2002": m_FileFormatVersion = "LM-63-2002"; break; case "IES:LM-63-2019": m_FileFormatVersion = "LM-63-2019"; break; default: m_FileFormatVersion = "LM-63-1986"; break; } var keywordRegex = new Regex(@"\s*\[(?\w+)\]\s*(?.*)", RegexOptions.Compiled); var tiltRegex = new Regex(@"TILT=(?.*)", RegexOptions.Compiled); string currentKeyword = string.Empty; for (string keywordLine = (m_FileFormatVersion == "LM-63-1986") ? versionLine : iesReader.ReadLine(); true; keywordLine = iesReader.ReadLine()) { if (keywordLine == null) { return "Premature end of file (missing TILT=NONE)."; } if (string.IsNullOrWhiteSpace(keywordLine)) { continue; } Match keywordMatch = keywordRegex.Match(keywordLine); if (keywordMatch.Success) { string keyword = keywordMatch.Groups["keyword"].Value; string data = keywordMatch.Groups["data"].Value.Trim(); if (keyword == currentKeyword || keyword == "MORE") { m_KeywordDictionary[currentKeyword] += $" {data}"; } else { // Many separate occurrences of keyword OTHER will need to be handled properly once exposed in the inspector. currentKeyword = keyword; m_KeywordDictionary[currentKeyword] = data; } continue; } Match tiltMatch = tiltRegex.Match(keywordLine); if (tiltMatch.Success) { string data = tiltMatch.Groups["data"].Value.Trim(); if (data == "NONE") { break; } return $"TILT format not supported: TILT={data}"; } } string[] iesDataTokens = Regex.Split(iesReader.ReadToEnd().Trim(), @"[\s,]+"); var iesDataTokenEnumerator = iesDataTokens.GetEnumerator(); string iesDataToken; if (iesDataTokens.Length == 1 && string.IsNullOrWhiteSpace(iesDataTokens[0])) { return "Premature end of file (missing IES data)."; } if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing lamp count value)."; } int lampCount; iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out lampCount)) { return $"Invalid lamp count value: {iesDataToken}"; } if (lampCount < 1) lampCount = 1; if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing lumens per lamp value)."; } float lumensPerLamp; iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out lumensPerLamp)) { return $"Invalid lumens per lamp value: {iesDataToken}"; } m_TotalLumens = (lumensPerLamp < 0f) ? -1f : lampCount * lumensPerLamp; if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing candela multiplier value)."; } float candelaMultiplier; iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out candelaMultiplier)) { return $"Invalid candela multiplier value: {iesDataToken}"; } if (candelaMultiplier < 0f) candelaMultiplier = 0f; if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing vertical angle count value)."; } iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_VerticalAngleCount)) { return $"Invalid vertical angle count value: {iesDataToken}"; } if (m_VerticalAngleCount < 1) { return $"Invalid number of vertical angles: {m_VerticalAngleCount}"; } if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing horizontal angle count value)."; } iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_HorizontalAngleCount)) { return $"Invalid horizontal angle count value: {iesDataToken}"; } if (m_HorizontalAngleCount < 1) { return $"Invalid number of horizontal angles: {m_HorizontalAngleCount}"; } if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing photometric type value)."; } iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_PhotometricType)) { return $"Invalid photometric type value: {iesDataToken}"; } if (m_PhotometricType < 1 || m_PhotometricType > 3) { return $"Invalid photometric type: {m_PhotometricType}"; } // Skip luminous dimension unit type. if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing luminous dimension unit type value)."; } // Skip luminous dimension width. if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing luminous dimension width value)."; } // Skip luminous dimension length. if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing luminous dimension length value)."; } // Skip luminous dimension height. if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing luminous dimension height value)."; } if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing ballast factor value)."; } float ballastFactor; iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out ballastFactor)) { return $"Invalid ballast factor value: {iesDataToken}"; } if (ballastFactor < 0f) ballastFactor = 0f; // Skip future use. if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing future use value)."; } // Skip input watts. if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing input watts value)."; } m_VerticalAngles = new float[m_VerticalAngleCount]; float previousVerticalAngle = float.MinValue; m_MinDeltaVerticalAngle = 180f; for (int v = 0; v < m_VerticalAngleCount; ++v) { if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing vertical angle values)."; } float angle; iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out angle)) { return $"Invalid vertical angle value: {iesDataToken}"; } if (angle <= previousVerticalAngle) { return $"Vertical angles are not in ascending order near: {angle}"; } float deltaVerticalAngle = angle - previousVerticalAngle; if (deltaVerticalAngle < m_MinDeltaVerticalAngle) { m_MinDeltaVerticalAngle = deltaVerticalAngle; } m_VerticalAngles[v] = previousVerticalAngle = angle; } m_HorizontalAngles = new float[m_HorizontalAngleCount]; float previousHorizontalAngle = float.MinValue; m_MinDeltaHorizontalAngle = 360f; for (int h = 0; h < m_HorizontalAngleCount; ++h) { if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing horizontal angle values)."; } float angle; iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out angle)) { return $"Invalid horizontal angle value: {iesDataToken}"; } if (angle <= previousHorizontalAngle) { return $"Horizontal angles are not in ascending order near: {angle}"; } float deltaHorizontalAngle = angle - previousHorizontalAngle; if (deltaHorizontalAngle < m_MinDeltaHorizontalAngle) { m_MinDeltaHorizontalAngle = deltaHorizontalAngle; } m_HorizontalAngles[h] = previousHorizontalAngle = angle; } m_FirstHorizontalAngle = m_HorizontalAngles[0]; m_LastHorizontalAngle = m_HorizontalAngles[m_HorizontalAngleCount - 1]; m_CandelaValues = new float[m_HorizontalAngleCount * m_VerticalAngleCount]; m_MaxCandelas = 0f; for (int h = 0; h < m_HorizontalAngleCount; ++h) { for (int v = 0; v < m_VerticalAngleCount; ++v) { if (!iesDataTokenEnumerator.MoveNext()) { return "Premature end of file (missing candela values)."; } float value; iesDataToken = iesDataTokenEnumerator.Current.ToString(); if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out value)) { return $"Invalid candela value: {iesDataToken}"; } value *= candelaMultiplier * ballastFactor; m_CandelaValues[h * m_VerticalAngleCount + v] = value; if (value > m_MaxCandelas) { m_MaxCandelas = value; } } } } return null; } internal string GetKeywordValue(string keyword) { return m_KeywordDictionary.ContainsKey(keyword) ? m_KeywordDictionary[keyword] : string.Empty; } internal int GetMinVerticalSampleCount() { if (m_PhotometricType == 2) // type B { // Factor in the 90 degree rotation that will be done when building the cylindrical texture. return 1 + (int)Mathf.Ceil(360 / m_MinDeltaHorizontalAngle); // 360 is 2 * 180 degrees } else // type A or C { return 1 + (int)Mathf.Ceil(360 / m_MinDeltaVerticalAngle); // 360 is 2 * 180 degrees } } internal int GetMinHorizontalSampleCount() { switch (m_PhotometricType) { case 3: // type A return 1 + (int)Mathf.Ceil(720 / m_MinDeltaHorizontalAngle); // 720 is 2 * 360 degrees case 2: // type B // Factor in the 90 degree rotation that will be done when building the cylindrical texture. return 1 + (int)Mathf.Ceil(720 / m_MinDeltaVerticalAngle); // 720 is 2 * 360 degrees default: // type C // Factor in the 90 degree rotation that will be done when building the cylindrical texture. return 1 + (int)Mathf.Ceil(720 / Mathf.Min(m_MinDeltaHorizontalAngle, m_MinDeltaVerticalAngle)); // 720 is 2 * 360 degrees } } internal float ComputeVerticalAnglePosition(float angle) { return ComputeAnglePosition(angle, m_VerticalAngles); } internal float ComputeTypeAorBHorizontalAnglePosition(float angle) // angle in range [-180..+180] degrees { return ComputeAnglePosition(((m_FirstHorizontalAngle == 0f) ? Mathf.Abs(angle) : angle), m_HorizontalAngles); } internal float ComputeTypeCHorizontalAnglePosition(float angle) // angle in range [0..360] degrees { switch (m_LastHorizontalAngle) { case 0f: // the luminaire is assumed to be laterally symmetric in all planes angle = 0f; break; case 90f: // the luminaire is assumed to be symmetric in each quadrant angle = 90f - Mathf.Abs(Mathf.Abs(angle - 180f) - 90f); break; case 180f: // the luminaire is assumed to be symmetric about the 0 to 180 degree plane angle = 180f - Mathf.Abs(angle - 180f); break; case 270f: angle = 270f - Mathf.Abs(Mathf.Abs(angle - 270f) - 180f); break; default: // the luminaire is assumed to exhibit no lateral symmetry break; } return ComputeAnglePosition(angle, m_HorizontalAngles); } internal float ComputeAnglePosition(float value, float[] angles) { int start = 0; int end = angles.Length - 1; if (value < angles[start]) { return start; } if (value > angles[end]) { return end; } while (start < end) { int index = (start + end + 1) / 2; float angle = angles[index]; if (value >= angle) { start = index; } else { end = index - 1; } } float leftValue = angles[start]; float fraction = 0f; if (start + 1 < angles.Length) { float rightValue = angles[start + 1]; float deltaValue = rightValue - leftValue; if (deltaValue > 0.0001f) { fraction = (value - leftValue) / deltaValue; } } return start + fraction; } internal float InterpolateBilinear(float x, float y) { int ix = (int)Mathf.Floor(x); int iy = (int)Mathf.Floor(y); float fractionX = x - ix; float fractionY = y - iy; float p00 = InterpolatePoint(ix + 0, iy + 0); float p10 = InterpolatePoint(ix + 1, iy + 0); float p01 = InterpolatePoint(ix + 0, iy + 1); float p11 = InterpolatePoint(ix + 1, iy + 1); float p0 = Mathf.Lerp(p00, p01, fractionY); float p1 = Mathf.Lerp(p10, p11, fractionY); return Mathf.Lerp(p0, p1, fractionX); } internal float InterpolatePoint(int x, int y) { x %= m_HorizontalAngles.Length; y %= m_VerticalAngles.Length; return m_CandelaValues[y + x * m_VerticalAngles.Length]; } } }