RampGenerator.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. // Copyright (c) 2008-2023 the Urho3D project
  2. // License: MIT
  3. #include <Urho3D/Container/ArrayPtr.h>
  4. #include <Urho3D/Core/Context.h>
  5. #include <Urho3D/IO/File.h>
  6. #include <Urho3D/IO/FileSystem.h>
  7. #include <Urho3D/Core/ProcessUtils.h>
  8. #include <Urho3D/Core/StringUtils.h>
  9. #ifdef WIN32
  10. #include <Urho3D/Engine/WinWrapped.h>
  11. #endif
  12. #include <STB/stb_image_write.h>
  13. #include <Urho3D/DebugNew.h>
  14. using namespace Urho3D;
  15. // Kernel used for blurring IES lights
  16. static const float sigma3Kernel9x9[9 * 9] = {
  17. 0.00401f, 0.005895f, 0.007763f, 0.009157f, 0.009675f, 0.009157f, 0.007763f, 0.005895f, 0.00401f,
  18. 0.005895f, 0.008667f, 0.011412f, 0.013461f, 0.014223f, 0.013461f, 0.011412f, 0.008667f, 0.005895f,
  19. 0.007763f, 0.011412f, 0.015028f, 0.017726f, 0.018729f, 0.017726f, 0.015028f, 0.011412f, 0.007763f,
  20. 0.009157f, 0.013461f, 0.017726f, 0.020909f, 0.022092f, 0.020909f, 0.017726f, 0.013461f, 0.009157f,
  21. 0.009675f, 0.014223f, 0.018729f, 0.022092f, 0.023342f, 0.022092f, 0.018729f, 0.014223f, 0.009675f,
  22. 0.009157f, 0.013461f, 0.017726f, 0.020909f, 0.022092f, 0.020909f, 0.017726f, 0.013461f, 0.009157f,
  23. 0.007763f, 0.011412f, 0.015028f, 0.017726f, 0.018729f, 0.017726f, 0.015028f, 0.011412f, 0.007763f,
  24. 0.005895f, 0.008667f, 0.011412f, 0.013461f, 0.014223f, 0.013461f, 0.011412f, 0.008667f, 0.005895f,
  25. 0.00401f, 0.005895f, 0.007763f, 0.009157f, 0.009675f, 0.009157f, 0.007763f, 0.005895f, 0.00401f
  26. };
  27. int main(int argc, char** argv);
  28. void Run(const Vector<String>& arguments);
  29. bool ReadIES(File* data, Vector<float>& vertical, Vector<float>& horizontal, Vector<float>& luminance);
  30. void WriteIES(unsigned char* data, unsigned width, unsigned height, Vector<float>& horizontal, Vector<float>& vertical, Vector<float>& luminance);
  31. void Blur(unsigned char* data, unsigned width, unsigned height, const float* kernel, unsigned kernelWidth);
  32. int main(int argc, char** argv)
  33. {
  34. Vector<String> arguments;
  35. #ifdef WIN32
  36. arguments = ParseArguments(GetCommandLineW());
  37. #else
  38. arguments = ParseArguments(argc, argv);
  39. #endif
  40. Run(arguments);
  41. return 0;
  42. }
  43. void Run(const Vector<String>& arguments)
  44. {
  45. if (arguments.Size() < 3)
  46. ErrorExit("Usage: RampGenerator <output png file> <width> <power> [dimensions]\n"
  47. "IES Usage: RampGenerator <input file> <output png file> <width> [dimensions]");
  48. if (GetExtension(arguments[0]) == ".ies") // Generate an IES light derived ramp
  49. {
  50. String inputFile = arguments[0];
  51. String ouputFile = arguments[1];
  52. int width = ToI32(arguments[2]);
  53. int dim = 1;
  54. if (arguments.Size() > 3)
  55. dim = ToI32(arguments[3]);
  56. int blurLevel = 0;
  57. if (arguments.Size() > 4)
  58. blurLevel = ToI32(arguments[4]);
  59. const int height = dim == 2 ? width : 1;
  60. Context context;
  61. File file(&context);
  62. file.Open(inputFile);
  63. Vector<float> horizontal;
  64. Vector<float> vertical;
  65. Vector<float> luminance;
  66. ReadIES(&file, vertical, horizontal, luminance);
  67. SharedArrayPtr<unsigned char> data(new unsigned char[width * height]);
  68. WriteIES(data, width, height, vertical, horizontal, luminance);
  69. // Apply a blur, simpler than interpolating through the 2 dimensions of coarse samples
  70. Blur(data, width, height, sigma3Kernel9x9, 9);
  71. stbi_write_png(arguments[1].CString(), width, height, 1, data.Get(), 0);
  72. }
  73. else // Generate a regular power based ramp
  74. {
  75. int width = ToI32(arguments[1]);
  76. float power = ToFloat(arguments[2]);
  77. int dimensions = 1;
  78. if (arguments.Size() > 3)
  79. dimensions = ToI32(arguments[3]);
  80. if (width < 2)
  81. ErrorExit("Width must be at least 2");
  82. if (dimensions < 1 || dimensions > 2)
  83. ErrorExit("Dimensions must be 1 or 2");
  84. if (dimensions == 1)
  85. {
  86. SharedArrayPtr<unsigned char> data(new unsigned char[width]);
  87. for (int i = 0; i < width; ++i)
  88. {
  89. float x = ((float)i) / ((float)(width - 1));
  90. data[i] = (unsigned char)((1.0f - Pow(x, power)) * 255.0f);
  91. }
  92. // Ensure start is full bright & end is completely black
  93. data[0] = 255;
  94. data[width - 1] = 0;
  95. stbi_write_png(arguments[0].CString(), width, 1, 1, data.Get(), 0);
  96. }
  97. if (dimensions == 2)
  98. {
  99. SharedArrayPtr<unsigned char> data(new unsigned char[width * width]);
  100. for (int y = 0; y < width; ++y)
  101. {
  102. for (int x = 0; x < width; ++x)
  103. {
  104. unsigned i = y * width + x;
  105. float halfWidth = width * 0.5f;
  106. float xf = (x - halfWidth + 0.5f) / (halfWidth - 0.5f);
  107. float yf = (y - halfWidth + 0.5f) / (halfWidth - 0.5f);
  108. float dist = sqrtf(xf * xf + yf * yf);
  109. if (dist > 1.0f)
  110. dist = 1.0f;
  111. data[i] = (unsigned char)((1.0f - Pow(dist, power)) * 255.0f);
  112. }
  113. }
  114. // Ensure the border is completely black
  115. for (int x = 0; x < width; ++x)
  116. {
  117. data[x] = 0;
  118. data[(width - 1) * width + x] = 0;
  119. data[x * width] = 0;
  120. data[x * width + (width - 1)] = 0;
  121. }
  122. stbi_write_png(arguments[0].CString(), width, width, 1, data.Get(), 0);
  123. }
  124. }
  125. }
  126. unsigned GetSample(float position, Vector<float>& inputs)
  127. {
  128. unsigned pos = 0;
  129. // Early outs
  130. if (position < inputs[0])
  131. return 0;
  132. else if (position > inputs.Back())
  133. return inputs.Size() - 1;
  134. // Find best candidate
  135. float closestVal = M_INFINITY;
  136. unsigned samplePos = -1;
  137. for (unsigned i = 0; i < inputs.Size(); ++i)
  138. {
  139. float val = inputs[i];
  140. float diff = Abs(val - position);
  141. if (diff < closestVal)
  142. {
  143. closestVal = diff;
  144. samplePos = i;
  145. }
  146. }
  147. return samplePos;
  148. }
  149. bool IsWhitespace(const String& string)
  150. {
  151. bool anyNot = false;
  152. for (unsigned i = 0; i < string.Length(); ++i)
  153. {
  154. if (!::isspace(string[i]))
  155. anyNot = true;
  156. }
  157. return !anyNot;
  158. }
  159. float PopFirstFloat(Vector<String>& words)
  160. {
  161. if (words.Size() > 0)
  162. {
  163. float ret = ToFloat(words[0]);
  164. words.Erase(0);
  165. return ret;
  166. }
  167. return -1.0f; // is < 0 ever valid?
  168. }
  169. int PopFirstInt(Vector<String>& words)
  170. {
  171. if (words.Size() > 0)
  172. {
  173. int ret = ToI32(words[0]);
  174. words.Erase(0);
  175. return ret;
  176. }
  177. return -1; // < 0 ever valid?
  178. }
  179. bool ReadIES(File* data, Vector<float>& vertical, Vector<float>& horizontal, Vector<float>& luminance)
  180. {
  181. String line = data->ReadLine();
  182. if (!line.Contains("IESNA:LM-63-1995") && !line.Contains("IESNA:LM-63-2002"))
  183. ErrorExit("Unsupported format: " + line);
  184. // Skip over the misc data
  185. while (!data->IsEof())
  186. {
  187. line = data->ReadLine();
  188. if (line.Contains("TILT=NONE"))
  189. break;
  190. else if (line.Contains("TILT=")) // tilt is a whole different ballgame
  191. ErrorExit("Unsupported tilt: " + line);
  192. else if (line.Contains("[")) // eat this line, it's metadata
  193. continue;
  194. }
  195. // Collect everything into a a list to process, we're now reading actual values
  196. Vector<String> lines;
  197. while (!data->IsEof())
  198. lines.Push(data->ReadLine());
  199. Vector<String> words;
  200. for (const String& line : lines)
  201. words.Push(line.Split(' '));
  202. // Prune any 'junk' collected
  203. for (unsigned i = 0; i < words.Size(); ++i)
  204. {
  205. if (words[i].Empty() || IsWhitespace(words[i]))
  206. {
  207. words.Erase(i);
  208. --i;
  209. }
  210. }
  211. const int sampleCount = PopFirstInt(words);
  212. const float lumens = PopFirstFloat(words);
  213. const float multiplier = PopFirstFloat(words); // Scales the candelas, used below
  214. const int verticalCount = PopFirstInt(words); //longitude
  215. const int horizontalCount = PopFirstInt(words); //latitude
  216. const int photometricType = PopFirstInt(words);
  217. const int measureType = PopFirstInt(words); // feet or meters
  218. const float width = PopFirstFloat(words);
  219. const float length = PopFirstFloat(words);
  220. const float height = PopFirstFloat(words);
  221. const float ballast = PopFirstFloat(words);
  222. PopFirstFloat(words); // Junk, called 'reserved' spot in spec
  223. PopFirstFloat(words); // Watts, unused
  224. for (int i = 0; i < verticalCount; ++i)
  225. {
  226. float value = PopFirstFloat(words);
  227. vertical.Push(value);
  228. }
  229. for (int i = 0; i < horizontalCount; ++i)
  230. {
  231. float value = PopFirstFloat(words);
  232. horizontal.Push(value);
  233. }
  234. for (int x = 0; x < horizontalCount; ++x)
  235. {
  236. for (int y = 0; y < verticalCount; ++y)
  237. luminance.Push(PopFirstFloat(words) * multiplier);
  238. }
  239. return true;
  240. }
  241. void WriteIES(unsigned char* data, unsigned width, unsigned height, Vector<float>& horizontal, Vector<float>& vertical, Vector<float>& luminance)
  242. {
  243. // Find maximum luminance value
  244. float maximum = -1;
  245. for (unsigned i = 0; i < luminance.Size(); ++i)
  246. maximum = Max(maximum, luminance[i]);
  247. // Find maximum radial slice
  248. float maxVert = 0;
  249. for (unsigned i = 0; i < vertical.Size(); ++i)
  250. maxVert = Max(maxVert, vertical[i]);
  251. // Find maximum altitude value
  252. float maxHoriz = 0;
  253. for (unsigned i = 0; i < horizontal.Size(); ++i)
  254. maxHoriz = Max(maxHoriz, horizontal[i]);
  255. const float inverseLightValue = 1.0f / maximum;
  256. const float inverseWidth = 1.0f / width;
  257. const float inverseHeight = 1.0f / height;
  258. float dirY = -1.0f;
  259. const float stepX = 2.0f / width;
  260. const float stepY = 2.0f / height;
  261. Vector3 centerVec(0, 0, 0);
  262. centerVec.Normalize();
  263. // Fitting to 90 degrees for better image usage
  264. // otherwise the space used would fit the light's traits and potentially incude a lot of wasted black space
  265. const float angularFactor = 90.0f;
  266. const float fraction = angularFactor / ((float)width);
  267. ::memset(data, 0, (size_t)width * height);
  268. for (unsigned y = 0; y < height; ++y)
  269. {
  270. float dirX = -1.0f;
  271. for (unsigned x = 0; x < width; ++x)
  272. {
  273. Vector3 dirVec(dirX * width, dirY * height, 0);
  274. const float len = dirVec.Length();
  275. const float weight = height > 1 ? Abs(1.0f - Cos(dirVec.Length() * fraction)) : x * inverseWidth;
  276. unsigned vert = GetSample(weight * angularFactor, horizontal);
  277. if (vert == -1)
  278. continue;
  279. float value = 0.0f;
  280. if (weight > 0.0f)
  281. {
  282. if (vertical.Size() == 1) // easy case
  283. value = luminance[vert];
  284. else
  285. {
  286. if (height > 1)
  287. {
  288. Vector3 normalized = dirVec.Normalized();
  289. float angle = Atan2(normalized.x_, normalized.y_) - maxHoriz;
  290. while (angle < 0)
  291. angle += 360.0f;
  292. const float moddedAngle = fmodf(angle, maxVert);
  293. unsigned horiz = GetSample(moddedAngle, vertical);
  294. value = luminance[vert + horizontal.Size() * horiz];
  295. }
  296. else
  297. {
  298. // Accumulate for an average across the radial slices
  299. for (unsigned i = 0; i < vertical.Size(); ++i)
  300. value += luminance[vert + i * vertical.Size()];
  301. value /= vertical.Size();
  302. }
  303. }
  304. }
  305. *data = (unsigned char)(inverseLightValue * value * 255.0f);
  306. ++data;
  307. dirX += stepX;
  308. }
  309. dirY += stepY;
  310. }
  311. }
  312. void Blur(unsigned char* data, unsigned width, unsigned height, const float* kernel, unsigned kernelWidth)
  313. {
  314. const int kernelDim = (kernelWidth / 2);
  315. for (int x = 0; x < width; ++x)
  316. {
  317. for (int y = 0; y < height; ++y)
  318. {
  319. float average = 0.0f;
  320. for (int filterX = 0; filterX < kernelWidth; ++filterX)
  321. {
  322. for (int filterY = 0; filterY < kernelWidth; ++filterY)
  323. {
  324. const int xSample = (x - kernelWidth / 2 + filterX + width) % width;
  325. const int ySample = (y - kernelWidth / 2 + filterY + height) % height;
  326. const float value = data[ySample + xSample * height] / 255.0f;
  327. average += value * kernel[filterY + filterX * kernelWidth];
  328. }
  329. }
  330. data[y + x * height] = average * 255.0f;
  331. }
  332. }
  333. }