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}