A game about forced loneliness, made by TACStudios
1using System.Collections.Generic; 2using System.Globalization; 3using System.IO; 4using System.Text.RegularExpressions; 5using UnityEngine; 6 7namespace UnityEditor.Rendering 8{ 9 /// <summary> 10 /// Class to Parse IES File 11 /// </summary> 12 [System.Serializable] 13 public class IESReader 14 { 15 string m_FileFormatVersion; 16 /// <summary> 17 /// Version of the IES File 18 /// </summary> 19 public string FileFormatVersion 20 { 21 get { return m_FileFormatVersion; } 22 } 23 24 float m_TotalLumens; 25 /// <summary> 26 /// Total light intensity (in Lumens) stored on the file, usage of it is optional (through the prefab subasset inside the IESObject) 27 /// </summary> 28 public float TotalLumens 29 { 30 get { return m_TotalLumens; } 31 } 32 33 float m_MaxCandelas; 34 /// <summary> 35 /// Maximum of Candela in the IES File 36 /// </summary> 37 public float MaxCandelas 38 { 39 get { return m_MaxCandelas; } 40 } 41 42 int m_PhotometricType; 43 44 /// <summary> 45 /// Type of Photometric light in the IES file, varying per IES-Type and version 46 /// </summary> 47 public int PhotometricType 48 { 49 get { return m_PhotometricType; } 50 } 51 52 Dictionary<string, string> m_KeywordDictionary = new Dictionary<string, string>(); 53 54 int m_VerticalAngleCount; 55 int m_HorizontalAngleCount; 56 float[] m_VerticalAngles; 57 float[] m_HorizontalAngles; 58 float[] m_CandelaValues; 59 60 float m_MinDeltaVerticalAngle; 61 float m_MinDeltaHorizontalAngle; 62 float m_FirstHorizontalAngle; 63 float m_LastHorizontalAngle; 64 65 // File format references: 66 // https://www.ies.org/product/standard-file-format-for-electronic-transfer-of-photometric-data/ 67 // http://lumen.iee.put.poznan.pl/kw/iesna.txt 68 // https://seblagarde.wordpress.com/2014/11/05/ies-light-format-specification-and-reader/ 69 /// <summary> 70 /// Main function to read the file 71 /// </summary> 72 /// <param name="iesFilePath">The path to the IES File on disk.</param> 73 /// <returns>Return the error during the import otherwise null if no error</returns> 74 public string ReadFile(string iesFilePath) 75 { 76 using (var iesReader = File.OpenText(iesFilePath)) 77 { 78 string versionLine = iesReader.ReadLine(); 79 80 if (versionLine == null) 81 { 82 return "Premature end of file (empty file)."; 83 } 84 85 switch (versionLine.Trim()) 86 { 87 case "IESNA91": 88 m_FileFormatVersion = "LM-63-1991"; 89 break; 90 case "IESNA:LM-63-1995": 91 m_FileFormatVersion = "LM-63-1995"; 92 break; 93 case "IESNA:LM-63-2002": 94 m_FileFormatVersion = "LM-63-2002"; 95 break; 96 case "IES:LM-63-2019": 97 m_FileFormatVersion = "LM-63-2019"; 98 break; 99 default: 100 m_FileFormatVersion = "LM-63-1986"; 101 break; 102 } 103 104 var keywordRegex = new Regex(@"\s*\[(?<keyword>\w+)\]\s*(?<data>.*)", RegexOptions.Compiled); 105 var tiltRegex = new Regex(@"TILT=(?<data>.*)", RegexOptions.Compiled); 106 107 string currentKeyword = string.Empty; 108 109 for (string keywordLine = (m_FileFormatVersion == "LM-63-1986") ? versionLine : iesReader.ReadLine(); true; keywordLine = iesReader.ReadLine()) 110 { 111 if (keywordLine == null) 112 { 113 return "Premature end of file (missing TILT=NONE)."; 114 } 115 116 if (string.IsNullOrWhiteSpace(keywordLine)) 117 { 118 continue; 119 } 120 121 Match keywordMatch = keywordRegex.Match(keywordLine); 122 123 if (keywordMatch.Success) 124 { 125 string keyword = keywordMatch.Groups["keyword"].Value; 126 string data = keywordMatch.Groups["data"].Value.Trim(); 127 128 if (keyword == currentKeyword || keyword == "MORE") 129 { 130 m_KeywordDictionary[currentKeyword] += $" {data}"; 131 } 132 else 133 { 134 // Many separate occurrences of keyword OTHER will need to be handled properly once exposed in the inspector. 135 currentKeyword = keyword; 136 m_KeywordDictionary[currentKeyword] = data; 137 } 138 139 continue; 140 } 141 142 Match tiltMatch = tiltRegex.Match(keywordLine); 143 144 if (tiltMatch.Success) 145 { 146 string data = tiltMatch.Groups["data"].Value.Trim(); 147 148 if (data == "NONE") 149 { 150 break; 151 } 152 153 return $"TILT format not supported: TILT={data}"; 154 } 155 } 156 157 string[] iesDataTokens = Regex.Split(iesReader.ReadToEnd().Trim(), @"[\s,]+"); 158 var iesDataTokenEnumerator = iesDataTokens.GetEnumerator(); 159 string iesDataToken; 160 161 162 if (iesDataTokens.Length == 1 && string.IsNullOrWhiteSpace(iesDataTokens[0])) 163 { 164 return "Premature end of file (missing IES data)."; 165 } 166 167 if (!iesDataTokenEnumerator.MoveNext()) 168 { 169 return "Premature end of file (missing lamp count value)."; 170 } 171 172 int lampCount; 173 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 174 if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out lampCount)) 175 { 176 return $"Invalid lamp count value: {iesDataToken}"; 177 } 178 if (lampCount < 1) lampCount = 1; 179 180 if (!iesDataTokenEnumerator.MoveNext()) 181 { 182 return "Premature end of file (missing lumens per lamp value)."; 183 } 184 185 float lumensPerLamp; 186 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 187 if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out lumensPerLamp)) 188 { 189 return $"Invalid lumens per lamp value: {iesDataToken}"; 190 } 191 m_TotalLumens = (lumensPerLamp < 0f) ? -1f : lampCount * lumensPerLamp; 192 193 if (!iesDataTokenEnumerator.MoveNext()) 194 { 195 return "Premature end of file (missing candela multiplier value)."; 196 } 197 198 float candelaMultiplier; 199 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 200 if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out candelaMultiplier)) 201 { 202 return $"Invalid candela multiplier value: {iesDataToken}"; 203 } 204 if (candelaMultiplier < 0f) candelaMultiplier = 0f; 205 206 if (!iesDataTokenEnumerator.MoveNext()) 207 { 208 return "Premature end of file (missing vertical angle count value)."; 209 } 210 211 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 212 if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_VerticalAngleCount)) 213 { 214 return $"Invalid vertical angle count value: {iesDataToken}"; 215 } 216 if (m_VerticalAngleCount < 1) 217 { 218 return $"Invalid number of vertical angles: {m_VerticalAngleCount}"; 219 } 220 221 if (!iesDataTokenEnumerator.MoveNext()) 222 { 223 return "Premature end of file (missing horizontal angle count value)."; 224 } 225 226 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 227 if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_HorizontalAngleCount)) 228 { 229 return $"Invalid horizontal angle count value: {iesDataToken}"; 230 } 231 if (m_HorizontalAngleCount < 1) 232 { 233 return $"Invalid number of horizontal angles: {m_HorizontalAngleCount}"; 234 } 235 236 if (!iesDataTokenEnumerator.MoveNext()) 237 { 238 return "Premature end of file (missing photometric type value)."; 239 } 240 241 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 242 if (!int.TryParse(iesDataToken, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out m_PhotometricType)) 243 { 244 return $"Invalid photometric type value: {iesDataToken}"; 245 } 246 if (m_PhotometricType < 1 || m_PhotometricType > 3) 247 { 248 return $"Invalid photometric type: {m_PhotometricType}"; 249 } 250 251 // Skip luminous dimension unit type. 252 if (!iesDataTokenEnumerator.MoveNext()) 253 { 254 return "Premature end of file (missing luminous dimension unit type value)."; 255 } 256 257 // Skip luminous dimension width. 258 if (!iesDataTokenEnumerator.MoveNext()) 259 { 260 return "Premature end of file (missing luminous dimension width value)."; 261 } 262 263 // Skip luminous dimension length. 264 if (!iesDataTokenEnumerator.MoveNext()) 265 { 266 return "Premature end of file (missing luminous dimension length value)."; 267 } 268 269 // Skip luminous dimension height. 270 if (!iesDataTokenEnumerator.MoveNext()) 271 { 272 return "Premature end of file (missing luminous dimension height value)."; 273 } 274 275 if (!iesDataTokenEnumerator.MoveNext()) 276 { 277 return "Premature end of file (missing ballast factor value)."; 278 } 279 280 float ballastFactor; 281 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 282 if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out ballastFactor)) 283 { 284 return $"Invalid ballast factor value: {iesDataToken}"; 285 } 286 if (ballastFactor < 0f) ballastFactor = 0f; 287 288 // Skip future use. 289 if (!iesDataTokenEnumerator.MoveNext()) 290 { 291 return "Premature end of file (missing future use value)."; 292 } 293 294 // Skip input watts. 295 if (!iesDataTokenEnumerator.MoveNext()) 296 { 297 return "Premature end of file (missing input watts value)."; 298 } 299 300 m_VerticalAngles = new float[m_VerticalAngleCount]; 301 float previousVerticalAngle = float.MinValue; 302 303 m_MinDeltaVerticalAngle = 180f; 304 305 for (int v = 0; v < m_VerticalAngleCount; ++v) 306 { 307 if (!iesDataTokenEnumerator.MoveNext()) 308 { 309 return "Premature end of file (missing vertical angle values)."; 310 } 311 312 float angle; 313 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 314 if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out angle)) 315 { 316 return $"Invalid vertical angle value: {iesDataToken}"; 317 } 318 319 if (angle <= previousVerticalAngle) 320 { 321 return $"Vertical angles are not in ascending order near: {angle}"; 322 } 323 324 float deltaVerticalAngle = angle - previousVerticalAngle; 325 if (deltaVerticalAngle < m_MinDeltaVerticalAngle) 326 { 327 m_MinDeltaVerticalAngle = deltaVerticalAngle; 328 } 329 330 m_VerticalAngles[v] = previousVerticalAngle = angle; 331 } 332 333 m_HorizontalAngles = new float[m_HorizontalAngleCount]; 334 float previousHorizontalAngle = float.MinValue; 335 336 m_MinDeltaHorizontalAngle = 360f; 337 338 for (int h = 0; h < m_HorizontalAngleCount; ++h) 339 { 340 if (!iesDataTokenEnumerator.MoveNext()) 341 { 342 return "Premature end of file (missing horizontal angle values)."; 343 } 344 345 float angle; 346 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 347 if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out angle)) 348 { 349 return $"Invalid horizontal angle value: {iesDataToken}"; 350 } 351 352 if (angle <= previousHorizontalAngle) 353 { 354 return $"Horizontal angles are not in ascending order near: {angle}"; 355 } 356 357 float deltaHorizontalAngle = angle - previousHorizontalAngle; 358 if (deltaHorizontalAngle < m_MinDeltaHorizontalAngle) 359 { 360 m_MinDeltaHorizontalAngle = deltaHorizontalAngle; 361 } 362 363 m_HorizontalAngles[h] = previousHorizontalAngle = angle; 364 } 365 366 m_FirstHorizontalAngle = m_HorizontalAngles[0]; 367 m_LastHorizontalAngle = m_HorizontalAngles[m_HorizontalAngleCount - 1]; 368 369 m_CandelaValues = new float[m_HorizontalAngleCount * m_VerticalAngleCount]; 370 m_MaxCandelas = 0f; 371 372 for (int h = 0; h < m_HorizontalAngleCount; ++h) 373 { 374 for (int v = 0; v < m_VerticalAngleCount; ++v) 375 { 376 if (!iesDataTokenEnumerator.MoveNext()) 377 { 378 return "Premature end of file (missing candela values)."; 379 } 380 381 float value; 382 iesDataToken = iesDataTokenEnumerator.Current.ToString(); 383 if (!float.TryParse(iesDataToken, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out value)) 384 { 385 return $"Invalid candela value: {iesDataToken}"; 386 } 387 value *= candelaMultiplier * ballastFactor; 388 389 m_CandelaValues[h * m_VerticalAngleCount + v] = value; 390 391 if (value > m_MaxCandelas) 392 { 393 m_MaxCandelas = value; 394 } 395 } 396 } 397 } 398 399 return null; 400 } 401 402 internal string GetKeywordValue(string keyword) 403 { 404 return m_KeywordDictionary.ContainsKey(keyword) ? m_KeywordDictionary[keyword] : string.Empty; 405 } 406 407 internal int GetMinVerticalSampleCount() 408 { 409 if (m_PhotometricType == 2) // type B 410 { 411 // Factor in the 90 degree rotation that will be done when building the cylindrical texture. 412 return 1 + (int)Mathf.Ceil(360 / m_MinDeltaHorizontalAngle); // 360 is 2 * 180 degrees 413 } 414 else // type A or C 415 { 416 return 1 + (int)Mathf.Ceil(360 / m_MinDeltaVerticalAngle); // 360 is 2 * 180 degrees 417 } 418 } 419 420 internal int GetMinHorizontalSampleCount() 421 { 422 switch (m_PhotometricType) 423 { 424 case 3: // type A 425 return 1 + (int)Mathf.Ceil(720 / m_MinDeltaHorizontalAngle); // 720 is 2 * 360 degrees 426 case 2: // type B 427 // Factor in the 90 degree rotation that will be done when building the cylindrical texture. 428 return 1 + (int)Mathf.Ceil(720 / m_MinDeltaVerticalAngle); // 720 is 2 * 360 degrees 429 default: // type C 430 // Factor in the 90 degree rotation that will be done when building the cylindrical texture. 431 return 1 + (int)Mathf.Ceil(720 / Mathf.Min(m_MinDeltaHorizontalAngle, m_MinDeltaVerticalAngle)); // 720 is 2 * 360 degrees 432 } 433 } 434 435 internal float ComputeVerticalAnglePosition(float angle) 436 { 437 return ComputeAnglePosition(angle, m_VerticalAngles); 438 } 439 440 internal float ComputeTypeAorBHorizontalAnglePosition(float angle) // angle in range [-180..+180] degrees 441 { 442 return ComputeAnglePosition(((m_FirstHorizontalAngle == 0f) ? Mathf.Abs(angle) : angle), m_HorizontalAngles); 443 } 444 445 internal float ComputeTypeCHorizontalAnglePosition(float angle) // angle in range [0..360] degrees 446 { 447 switch (m_LastHorizontalAngle) 448 { 449 case 0f: // the luminaire is assumed to be laterally symmetric in all planes 450 angle = 0f; 451 break; 452 case 90f: // the luminaire is assumed to be symmetric in each quadrant 453 angle = 90f - Mathf.Abs(Mathf.Abs(angle - 180f) - 90f); 454 break; 455 case 180f: // the luminaire is assumed to be symmetric about the 0 to 180 degree plane 456 angle = 180f - Mathf.Abs(angle - 180f); 457 break; 458 case 270f: 459 angle = 270f - Mathf.Abs(Mathf.Abs(angle - 270f) - 180f); 460 break; 461 default: // the luminaire is assumed to exhibit no lateral symmetry 462 break; 463 } 464 465 return ComputeAnglePosition(angle, m_HorizontalAngles); 466 } 467 468 internal float ComputeAnglePosition(float value, float[] angles) 469 { 470 int start = 0; 471 int end = angles.Length - 1; 472 473 if (value < angles[start]) 474 { 475 return start; 476 } 477 478 if (value > angles[end]) 479 { 480 return end; 481 } 482 483 while (start < end) 484 { 485 int index = (start + end + 1) / 2; 486 487 float angle = angles[index]; 488 489 if (value >= angle) 490 { 491 start = index; 492 } 493 else 494 { 495 end = index - 1; 496 } 497 } 498 499 float leftValue = angles[start]; 500 float fraction = 0f; 501 502 if (start + 1 < angles.Length) 503 { 504 float rightValue = angles[start + 1]; 505 float deltaValue = rightValue - leftValue; 506 507 if (deltaValue > 0.0001f) 508 { 509 fraction = (value - leftValue) / deltaValue; 510 } 511 } 512 513 return start + fraction; 514 } 515 516 internal float InterpolateBilinear(float x, float y) 517 { 518 int ix = (int)Mathf.Floor(x); 519 int iy = (int)Mathf.Floor(y); 520 521 float fractionX = x - ix; 522 float fractionY = y - iy; 523 524 float p00 = InterpolatePoint(ix + 0, iy + 0); 525 float p10 = InterpolatePoint(ix + 1, iy + 0); 526 float p01 = InterpolatePoint(ix + 0, iy + 1); 527 float p11 = InterpolatePoint(ix + 1, iy + 1); 528 529 float p0 = Mathf.Lerp(p00, p01, fractionY); 530 float p1 = Mathf.Lerp(p10, p11, fractionY); 531 532 return Mathf.Lerp(p0, p1, fractionX); 533 } 534 535 internal float InterpolatePoint(int x, int y) 536 { 537 x %= m_HorizontalAngles.Length; 538 y %= m_VerticalAngles.Length; 539 540 return m_CandelaValues[y + x * m_VerticalAngles.Length]; 541 } 542 } 543}