CSplat.cpp 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218
  1. #include "CSplat.h"
  2. #include <algorithm>
  3. #include <cmath>
  4. #include <condition_variable>
  5. #include <cstdio>
  6. #include <cstring>
  7. #include <ctime>
  8. #include <limits>
  9. #include <mutex>
  10. #include <optional>
  11. #include <thread>
  12. #include <glm/gtc/constants.hpp>
  13. #include <glm/gtc/matrix_transform.hpp>
  14. #include <glm/gtc/type_ptr.hpp>
  15. #include "WallpaperEngine/Application/ApplicationContext.h"
  16. #include "WallpaperEngine/Application/WallpaperApplication.h"
  17. #include "WallpaperEngine/Data/Model/Project.h"
  18. #include "WallpaperEngine/Data/Model/Property.h"
  19. #include "WallpaperEngine/Data/Model/Wallpaper.h"
  20. #include "WallpaperEngine/Logging/Log.h"
  21. #include "WallpaperEngine/Splat/SogLoader.h"
  22. using namespace WallpaperEngine::Render;
  23. using namespace WallpaperEngine::Render::Wallpapers;
  24. namespace WallpaperEngine::Render::Wallpapers {
  25. // Orders splats back to front along the view direction with a 16 bit counting sort on a worker thread,
  26. // the result is a list of splat indices to draw as instances.
  27. class SplatSorter {
  28. public:
  29. SplatSorter (std::vector<glm::vec3> centers, std::vector<uint32_t> ids) :
  30. m_centers (std::move (centers)), m_ids (std::move (ids)) {
  31. this->m_worker = std::thread (&SplatSorter::run, this);
  32. }
  33. ~SplatSorter () {
  34. {
  35. std::lock_guard lock (this->m_mutex);
  36. this->m_quit = true;
  37. }
  38. this->m_wake.notify_one ();
  39. this->m_worker.join ();
  40. }
  41. void sortNow (const glm::vec3& position, const glm::vec3& forward, std::vector<uint32_t>& out) const {
  42. Scratch scratch;
  43. this->sort (position, forward, scratch, out);
  44. }
  45. // Asks the worker for a new order; if it is still busy with an older request, that one is replaced
  46. void request (const glm::vec3& position, const glm::vec3& forward) {
  47. {
  48. std::lock_guard lock (this->m_mutex);
  49. this->m_requestPosition = position;
  50. this->m_requestForward = forward;
  51. this->m_hasRequest = true;
  52. }
  53. this->m_wake.notify_one ();
  54. }
  55. bool takeResult (std::vector<uint32_t>& out) {
  56. std::lock_guard lock (this->m_mutex);
  57. if (!this->m_hasResult) {
  58. return false;
  59. }
  60. out = std::move (this->m_result);
  61. this->m_hasResult = false;
  62. return true;
  63. }
  64. private:
  65. struct Scratch {
  66. std::vector<float> depths;
  67. std::vector<uint32_t> keys;
  68. std::vector<uint32_t> counts;
  69. };
  70. void sort (const glm::vec3& position, const glm::vec3& forward, Scratch& scratch, std::vector<uint32_t>& out) const {
  71. constexpr float nearPlane = 0.05f;
  72. constexpr uint32_t bucketCount = 65536;
  73. constexpr uint32_t skipped = 0xFFFFFFFFu;
  74. const size_t count = this->m_centers.size ();
  75. scratch.depths.resize (count);
  76. scratch.keys.resize (count);
  77. scratch.counts.assign (bucketCount + 1, 0);
  78. float nearest = std::numeric_limits<float>::max ();
  79. float farthest = std::numeric_limits<float>::lowest ();
  80. for (size_t i = 0; i < count; i++) {
  81. const float depth = glm::dot (this->m_centers[i] - position, forward);
  82. scratch.depths[i] = depth;
  83. if (depth > nearPlane) {
  84. nearest = std::min (nearest, depth);
  85. farthest = std::max (farthest, depth);
  86. }
  87. }
  88. out.clear ();
  89. if (farthest < nearest) {
  90. return;
  91. }
  92. const float scale = static_cast<float> (bucketCount - 1) / std::max (farthest - nearest, 1e-6f);
  93. size_t visible = 0;
  94. for (size_t i = 0; i < count; i++) {
  95. if (scratch.depths[i] <= nearPlane) {
  96. scratch.keys[i] = skipped;
  97. continue;
  98. }
  99. // farthest first, so the largest depth lands in bucket 0
  100. const uint32_t key = static_cast<uint32_t> ((farthest - scratch.depths[i]) * scale);
  101. scratch.keys[i] = std::min (key, bucketCount - 1);
  102. scratch.counts[scratch.keys[i] + 1]++;
  103. visible++;
  104. }
  105. for (uint32_t bucket = 0; bucket < bucketCount; bucket++) {
  106. scratch.counts[bucket + 1] += scratch.counts[bucket];
  107. }
  108. out.resize (visible);
  109. for (size_t i = 0; i < count; i++) {
  110. if (scratch.keys[i] != skipped) {
  111. out[scratch.counts[scratch.keys[i]]++] = this->m_ids[i];
  112. }
  113. }
  114. }
  115. void run () {
  116. Scratch scratch;
  117. std::unique_lock lock (this->m_mutex);
  118. while (true) {
  119. this->m_wake.wait (lock, [this] { return this->m_quit || this->m_hasRequest; });
  120. if (this->m_quit) {
  121. return;
  122. }
  123. const glm::vec3 position = this->m_requestPosition;
  124. const glm::vec3 forward = this->m_requestForward;
  125. this->m_hasRequest = false;
  126. lock.unlock ();
  127. std::vector<uint32_t> result;
  128. this->sort (position, forward, scratch, result);
  129. lock.lock ();
  130. this->m_result = std::move (result);
  131. this->m_hasResult = true;
  132. }
  133. }
  134. const std::vector<glm::vec3> m_centers;
  135. const std::vector<uint32_t> m_ids;
  136. std::mutex m_mutex;
  137. std::condition_variable m_wake;
  138. glm::vec3 m_requestPosition = {};
  139. glm::vec3 m_requestForward = {};
  140. bool m_hasRequest = false;
  141. std::vector<uint32_t> m_result;
  142. bool m_hasResult = false;
  143. bool m_quit = false;
  144. std::thread m_worker;
  145. };
  146. } // namespace WallpaperEngine::Render::Wallpapers
  147. namespace {
  148. // Gaussian splat rasterizer: camera facing quad instances, the screen space ellipse comes from the projected 3D covariance (EWA splatting).
  149. const char* SPLAT_VERTEX_SHADER = R"(#version 330
  150. precision highp float;
  151. uniform usampler2D u_Centers;
  152. uniform sampler2D u_Rotations;
  153. uniform sampler2D u_Scales;
  154. uniform mat4 u_View;
  155. uniform mat4 u_Projection;
  156. uniform vec2 u_Viewport;
  157. uniform vec2 u_Focal;
  158. uniform int u_TextureWidth;
  159. // The clock: a seven segment display in screen space. Splats inside a digit segment are inverted and float up,
  160. // the splats where the digits end up are faded out so they read against the background.
  161. uniform vec4 u_ClockPoints[32];
  162. uniform vec4 u_ClockParams;
  163. uniform vec4 u_ClockBounds;
  164. uniform float u_ClockDistance;
  165. in vec2 a_Corner;
  166. in uint a_Index;
  167. out vec4 v_Color;
  168. out vec2 v_Corner;
  169. float clockField (vec2 screen, vec2 maskOffset) {
  170. vec2 toCircle = vec2 (max (1.0, u_ClockParams.w), 1.0);
  171. float radius = u_ClockParams.z;
  172. float field = 0.0;
  173. for (int i = 0; i < 32; i++) {
  174. vec4 point = u_ClockPoints[i];
  175. if (abs (point.z) <= 0.001) {
  176. continue;
  177. }
  178. vec2 delta = (screen - (point.xy + maskOffset)) * toCircle;
  179. float halfLength = abs (point.w);
  180. float dist = length (delta);
  181. if (halfLength > 0.0001) {
  182. dist = point.w >= 0.0
  183. ? length (vec2 (max (abs (delta.x) - halfLength, 0.0), delta.y))
  184. : length (vec2 (delta.x, max (abs (delta.y) - halfLength, 0.0)));
  185. }
  186. float influence = dist <= radius ? point.z : 0.0;
  187. if (abs (influence) > abs (field)) {
  188. field = influence;
  189. }
  190. }
  191. return field;
  192. }
  193. void main () {
  194. ivec2 texel = ivec2 (int (a_Index) % u_TextureWidth, int (a_Index) / u_TextureWidth);
  195. uvec4 packedCenter = texelFetch (u_Centers, texel, 0);
  196. vec3 center = uintBitsToFloat (packedCenter.xyz);
  197. vec4 camera = u_View * vec4 (center, 1.0);
  198. float depth = -camera.z;
  199. vec4 clip = u_Projection * camera;
  200. if (depth < 0.05 || abs (clip.x) > clip.w * 1.3 || abs (clip.y) > clip.w * 1.3) {
  201. gl_Position = vec4 (0.0, 0.0, 2.0, 1.0);
  202. v_Color = vec4 (0.0);
  203. v_Corner = vec2 (0.0);
  204. return;
  205. }
  206. vec4 q = texelFetch (u_Rotations, texel, 0);
  207. vec3 s = texelFetch (u_Scales, texel, 0).xyz;
  208. float x = q.x, y = q.y, z = q.z, w = q.w;
  209. mat3 rotation = mat3 (
  210. 1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y + w * z), 2.0 * (x * z - w * y),
  211. 2.0 * (x * y - w * z), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z + w * x),
  212. 2.0 * (x * z + w * y), 2.0 * (y * z - w * x), 1.0 - 2.0 * (x * x + y * y)
  213. );
  214. mat3 axes = mat3 (u_View) * mat3 (rotation[0] * s.x, rotation[1] * s.y, rotation[2] * s.z);
  215. mat3 covariance = axes * transpose (axes);
  216. float invDepth = 1.0 / depth;
  217. vec3 jacobianX = vec3 (u_Focal.x * invDepth, 0.0, u_Focal.x * camera.x * invDepth * invDepth);
  218. vec3 jacobianY = vec3 (0.0, u_Focal.y * invDepth, u_Focal.y * camera.y * invDepth * invDepth);
  219. vec3 covarianceX = covariance * jacobianX;
  220. vec3 covarianceY = covariance * jacobianY;
  221. // the 0.3 is the usual low pass filter that keeps sub-pixel splats from vanishing
  222. float a = dot (jacobianX, covarianceX) + 0.3;
  223. float b = dot (jacobianX, covarianceY);
  224. float c = dot (jacobianY, covarianceY) + 0.3;
  225. float mid = 0.5 * (a + c);
  226. float radius = length (vec2 (0.5 * (a - c), b));
  227. float lambda1 = mid + radius;
  228. float lambda2 = max (mid - radius, 0.1);
  229. vec2 direction = abs (b) > 1e-6 ? normalize (vec2 (b, lambda1 - a)) : (a >= c ? vec2 (1.0, 0.0) : vec2 (0.0, 1.0));
  230. vec2 major = min (sqrt (2.0 * lambda1), 1024.0) * direction;
  231. vec2 minor = min (sqrt (2.0 * lambda2), 1024.0) * vec2 (direction.y, -direction.x);
  232. vec2 offset = a_Corner.x * major + a_Corner.y * minor;
  233. vec2 centerNdc = clip.xy / clip.w;
  234. vec2 ndc = centerNdc + offset * 2.0 / u_Viewport;
  235. uint rgba = packedCenter.w;
  236. v_Color = vec4 (float (rgba & 255u), float ((rgba >> 8) & 255u), float ((rgba >> 16) & 255u), float (rgba >> 24)) / 255.0;
  237. vec2 screen = vec2 (centerNdc.x * 0.5 + 0.5, 0.5 - centerNdc.y * 0.5);
  238. if (u_ClockParams.x > 0.0 && screen.x >= u_ClockBounds.x && screen.y >= u_ClockBounds.y
  239. && screen.x <= u_ClockBounds.z && screen.y <= u_ClockBounds.w) {
  240. float radius = u_ClockParams.z;
  241. float lift = u_ClockParams.y;
  242. float influence = clockField (screen, vec2 (0.0));
  243. if (influence > 0.001) {
  244. v_Color.rgb = 1.0 - v_Color.rgb;
  245. } else {
  246. float destination = max (0.0, clockField (screen, vec2 (0.0, -radius * (0.58 + 2.35) * lift)));
  247. v_Color.a *= 1.0 - clamp (u_ClockDistance * destination * 0.72, 0.0, 0.96);
  248. }
  249. if (abs (influence) > 0.001) {
  250. float animation = abs (influence);
  251. float seed = fract (sin ((float (a_Index) * 29.29 + 7.7) * 12.9898) * 43758.5453);
  252. float liftPhase = smoothstep (0.0, 0.48, animation);
  253. float risePhase = smoothstep (0.34, 1.0, animation);
  254. float verticalSign = influence >= 0.0 ? -1.0 : 1.0;
  255. vec2 screenOffset = vec2 (
  256. (seed - 0.5) * radius * 0.10, verticalSign * radius * (0.58 * liftPhase + 2.35 * risePhase)
  257. ) * lift;
  258. // screen space is y down, clip space y up
  259. ndc += vec2 (screenOffset.x * 2.0, -screenOffset.y * 2.0);
  260. }
  261. }
  262. // the engine samples wallpaper textures with row 0 at the top, but a GL render target stores row
  263. // 0 at the bottom, so the whole frame is drawn upside down here
  264. gl_Position = vec4 (ndc.x, -ndc.y, clip.z / clip.w, 1.0);
  265. v_Corner = a_Corner;
  266. }
  267. )";
  268. const char* SPLAT_FRAGMENT_SHADER = R"(#version 330
  269. precision highp float;
  270. in vec4 v_Color;
  271. in vec2 v_Corner;
  272. out vec4 out_FragColor;
  273. void main () {
  274. float falloff = -dot (v_Corner, v_Corner);
  275. if (falloff < -4.0) {
  276. discard;
  277. }
  278. float alpha = exp (falloff) * v_Color.a;
  279. if (alpha < 1.0 / 255.0) {
  280. discard;
  281. }
  282. out_FragColor = vec4 (v_Color.rgb, alpha);
  283. }
  284. )";
  285. GLuint compileShader (GLenum type, const char* source) {
  286. const GLuint shader = glCreateShader (type);
  287. glShaderSource (shader, 1, &source, nullptr);
  288. glCompileShader (shader);
  289. GLint status = GL_FALSE;
  290. glGetShaderiv (shader, GL_COMPILE_STATUS, &status);
  291. if (status != GL_TRUE) {
  292. GLint length = 0;
  293. glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &length);
  294. std::string log (std::max (length, 1), '\0');
  295. glGetShaderInfoLog (shader, length, nullptr, log.data ());
  296. glDeleteShader (shader);
  297. sLog.exception ("Cannot compile the splat shader: ", log);
  298. }
  299. return shader;
  300. }
  301. float quantile (const std::vector<float>& sorted, float fraction) {
  302. const auto index = static_cast<long> (std::lround ((sorted.size () - 1) * fraction));
  303. return sorted[std::clamp<long> (index, 0, static_cast<long> (sorted.size ()) - 1)];
  304. }
  305. // Horizontal/vertical extent of the frustum at unit distance, given the wallpaper camera's fov
  306. // (horizontal for landscape viewports, vertical for portrait, matching the viewer)
  307. glm::vec2 frustumTangents (float fovDegrees, int width, int height) {
  308. const float w = static_cast<float> (std::max (width, 1));
  309. const float h = static_cast<float> (std::max (height, 1));
  310. const float aspect = w / h;
  311. const float tangent = std::tan (std::clamp (fovDegrees, 1.0f, 179.0f) * glm::pi<float> () / 360.0f);
  312. return w > h ? glm::vec2 (tangent, tangent / aspect) : glm::vec2 (tangent * aspect, tangent);
  313. }
  314. } // namespace
  315. bool CSplat::supports (const Project& project) {
  316. const auto& properties = project.properties;
  317. return properties.contains ("sogPreset") && properties.contains ("sogmeta") && properties.contains ("sogdirectory");
  318. }
  319. CSplat::CSplat (
  320. const Wallpaper& wallpaper, RenderContext& context, AudioContext& audioContext,
  321. const WallpaperState::TextureUVsScaling& scalingMode, const uint32_t& clampMode
  322. ) : CWallpaper (wallpaper, context, audioContext, scalingMode, clampMode) {
  323. this->setupFramebuffers ();
  324. this->loadCloud ();
  325. }
  326. CSplat::~CSplat () {
  327. // stops the worker before the buffers it never touches are freed
  328. this->m_sorter.reset ();
  329. glDeleteProgram (this->m_program);
  330. glDeleteVertexArrays (1, &this->m_vao);
  331. glDeleteBuffers (1, &this->m_cornerBuffer);
  332. glDeleteBuffers (1, &this->m_orderBuffer);
  333. glDeleteTextures (1, &this->m_centerTexture);
  334. glDeleteTextures (1, &this->m_rotationTexture);
  335. glDeleteTextures (1, &this->m_scaleTexture);
  336. }
  337. double CSplat::numberProperty (const std::string& name, double fallback) const {
  338. const auto& properties = this->getWallpaperData ().project.properties;
  339. const auto property = properties.find (name);
  340. if (property == properties.end ()) {
  341. return fallback;
  342. }
  343. const std::string text = property->second->toString ();
  344. if (text == "true") {
  345. return 1.0;
  346. }
  347. if (text == "false") {
  348. return 0.0;
  349. }
  350. try {
  351. return std::stod (text);
  352. } catch (const std::exception&) {
  353. return fallback;
  354. }
  355. }
  356. std::string CSplat::stringProperty (const std::string& name) const {
  357. const auto& properties = this->getWallpaperData ().project.properties;
  358. const auto property = properties.find (name);
  359. return property == properties.end () ? std::string () : property->second->toString ();
  360. }
  361. void CSplat::loadCloud () {
  362. const auto& locator = *this->getWallpaperData ().project.assetLocator;
  363. // same source selection as the wallpaper's own page: a workshop preset carries its own splat
  364. // data (inline meta + a directory of images), everything else uses one of the bundled scenes
  365. std::string meta = this->stringProperty ("sogmeta");
  366. std::string directory = this->stringProperty ("sogdirectory");
  367. std::replace (directory.begin (), directory.end (), '\\', '/');
  368. while (!directory.empty () && directory.back () == '/') {
  369. directory.pop_back ();
  370. }
  371. this->m_containFraming = !meta.empty () && !directory.empty ();
  372. if (meta.empty () || directory.empty ()) {
  373. const int preset = static_cast<int> (this->numberProperty ("sogPreset", 1));
  374. if (preset != 1 && preset != 2) {
  375. sLog.exception ("This SOG wallpaper uses a custom splat file, which the native renderer cannot load");
  376. }
  377. directory = "imgs/" + std::to_string (preset);
  378. meta = locator.readString (directory + "/meta.json");
  379. }
  380. Splat::SplatCloud cloud;
  381. try {
  382. cloud = Splat::loadSog (meta, [&] (const std::string& name) { return locator.readString (directory + "/" + name); });
  383. } catch (const std::exception& e) {
  384. sLog.exception ("Cannot load the splat cloud from ", directory, ": ", e.what ());
  385. }
  386. this->m_cloudCount = cloud.count;
  387. this->m_textureWidth = cloud.textureWidth;
  388. // The viewer hangs the splats off an entity rotated 180 degrees around Z (the photo frame has y
  389. // pointing down), so its camera math all happens in that frame: (x, y, z) -> (-x, -y, z).
  390. std::vector<glm::vec3> worldCenters;
  391. std::vector<uint32_t> drawIds;
  392. worldCenters.reserve (cloud.count);
  393. drawIds.reserve (cloud.count);
  394. std::vector<float> horizontalAngles;
  395. std::vector<float> verticalAngles;
  396. std::vector<float> depths;
  397. const uint32_t step = std::max (1u, (cloud.count + 119999) / 120000);
  398. glm::vec3 boundsMin (std::numeric_limits<float>::max ());
  399. glm::vec3 boundsMax (std::numeric_limits<float>::lowest ());
  400. for (uint32_t i = 0; i < cloud.count; i++) {
  401. const float* centerAndColor = &cloud.centerAndColor[static_cast<size_t> (i) * 4];
  402. const glm::vec3 world (-centerAndColor[0], -centerAndColor[1], centerAndColor[2]);
  403. boundsMin = glm::min (boundsMin, world);
  404. boundsMax = glm::max (boundsMax, world);
  405. uint32_t packedColor;
  406. std::memcpy (&packedColor, &centerAndColor[3], sizeof (packedColor));
  407. // fully transparent splats never contribute a pixel
  408. if ((packedColor >> 24) != 0) {
  409. worldCenters.push_back (world);
  410. drawIds.push_back (i);
  411. }
  412. if (i % step == 0 && world.z > 1e-4f) {
  413. horizontalAngles.push_back (std::atan2 (world.x, world.z));
  414. verticalAngles.push_back (std::atan2 (world.y, world.z));
  415. depths.push_back (world.z);
  416. }
  417. }
  418. this->m_profile.boundsCenter = (boundsMin + boundsMax) * 0.5f;
  419. this->m_profile.boundsHalfExtents = glm::max ((boundsMax - boundsMin) * 0.5f, glm::vec3 (0.001f));
  420. if (depths.size () >= 128) {
  421. std::sort (horizontalAngles.begin (), horizontalAngles.end ());
  422. std::sort (verticalAngles.begin (), verticalAngles.end ());
  423. std::sort (depths.begin (), depths.end ());
  424. const float horizontalMin = quantile (horizontalAngles, 0.02f);
  425. const float horizontalMax = quantile (horizontalAngles, 0.98f);
  426. const float verticalMin = quantile (verticalAngles, 0.02f);
  427. const float verticalMax = quantile (verticalAngles, 0.98f);
  428. this->m_profile.direction = glm::normalize (glm::vec3 (
  429. std::tan ((horizontalMin + horizontalMax) * 0.5f), std::tan ((verticalMin + verticalMax) * 0.5f), 1.0f
  430. ));
  431. this->m_profile.angularSize
  432. = { std::max (horizontalMax - horizontalMin, 0.01f), std::max (verticalMax - verticalMin, 0.01f) };
  433. this->m_profile.depthMin = quantile (depths, 0.02f);
  434. this->m_profile.depthMax = std::max (quantile (depths, 0.98f), this->m_profile.depthMin + 0.01f);
  435. } else {
  436. sLog.error ("SOG wallpaper has too few splats in front of the camera to derive its framing, using defaults");
  437. }
  438. sLog.out (
  439. "Loaded ", cloud.count, " splats (", drawIds.size (), " visible), depth ", this->m_profile.depthMin, " - ",
  440. this->m_profile.depthMax
  441. );
  442. this->setupGL (cloud.centerAndColor, cloud.rotation, cloud.scale);
  443. this->m_sorter = std::make_unique<SplatSorter> (std::move (worldCenters), std::move (drawIds));
  444. }
  445. void CSplat::setupGL (
  446. const std::vector<float>& centers, const std::vector<float>& rotations, const std::vector<float>& scales
  447. ) {
  448. const GLsizei width = static_cast<GLsizei> (this->m_textureWidth);
  449. const GLsizei height = static_cast<GLsizei> (centers.size () / 4 / this->m_textureWidth);
  450. const auto makeTexture = [] (GLuint& texture) {
  451. glGenTextures (1, &texture);
  452. glBindTexture (GL_TEXTURE_2D, texture);
  453. // integer textures cannot be filtered, and the shader only ever texelFetch()es anyway
  454. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
  455. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  456. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  457. glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  458. };
  459. // integer texture, a float one may flush denormals and eat some colors
  460. makeTexture (this->m_centerTexture);
  461. glTexImage2D (
  462. GL_TEXTURE_2D, 0, GL_RGBA32UI, width, height, 0, GL_RGBA_INTEGER, GL_UNSIGNED_INT, centers.data ()
  463. );
  464. makeTexture (this->m_rotationTexture);
  465. glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, rotations.data ());
  466. makeTexture (this->m_scaleTexture);
  467. glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, scales.data ());
  468. glBindTexture (GL_TEXTURE_2D, 0);
  469. const GLuint vertexShader = compileShader (GL_VERTEX_SHADER, SPLAT_VERTEX_SHADER);
  470. const GLuint fragmentShader = compileShader (GL_FRAGMENT_SHADER, SPLAT_FRAGMENT_SHADER);
  471. this->m_program = glCreateProgram ();
  472. glAttachShader (this->m_program, vertexShader);
  473. glAttachShader (this->m_program, fragmentShader);
  474. glLinkProgram (this->m_program);
  475. GLint linked = GL_FALSE;
  476. glGetProgramiv (this->m_program, GL_LINK_STATUS, &linked);
  477. if (linked != GL_TRUE) {
  478. GLint length = 0;
  479. glGetProgramiv (this->m_program, GL_INFO_LOG_LENGTH, &length);
  480. std::string log (std::max (length, 1), '\0');
  481. glGetProgramInfoLog (this->m_program, length, nullptr, log.data ());
  482. sLog.exception ("Cannot link the splat shader: ", log);
  483. }
  484. glDetachShader (this->m_program, vertexShader);
  485. glDetachShader (this->m_program, fragmentShader);
  486. glDeleteShader (vertexShader);
  487. glDeleteShader (fragmentShader);
  488. this->u_Centers = glGetUniformLocation (this->m_program, "u_Centers");
  489. this->u_Rotations = glGetUniformLocation (this->m_program, "u_Rotations");
  490. this->u_Scales = glGetUniformLocation (this->m_program, "u_Scales");
  491. this->u_View = glGetUniformLocation (this->m_program, "u_View");
  492. this->u_Projection = glGetUniformLocation (this->m_program, "u_Projection");
  493. this->u_Viewport = glGetUniformLocation (this->m_program, "u_Viewport");
  494. this->u_Focal = glGetUniformLocation (this->m_program, "u_Focal");
  495. this->u_TextureWidth = glGetUniformLocation (this->m_program, "u_TextureWidth");
  496. this->u_ClockPoints = glGetUniformLocation (this->m_program, "u_ClockPoints");
  497. this->u_ClockParams = glGetUniformLocation (this->m_program, "u_ClockParams");
  498. this->u_ClockBounds = glGetUniformLocation (this->m_program, "u_ClockBounds");
  499. this->u_ClockDistance = glGetUniformLocation (this->m_program, "u_ClockDistance");
  500. const GLint cornerAttribute = glGetAttribLocation (this->m_program, "a_Corner");
  501. const GLint indexAttribute = glGetAttribLocation (this->m_program, "a_Index");
  502. constexpr GLfloat corners[] = { -2.0f, -2.0f, 2.0f, -2.0f, -2.0f, 2.0f, 2.0f, 2.0f };
  503. glGenVertexArrays (1, &this->m_vao);
  504. glBindVertexArray (this->m_vao);
  505. glGenBuffers (1, &this->m_cornerBuffer);
  506. glBindBuffer (GL_ARRAY_BUFFER, this->m_cornerBuffer);
  507. glBufferData (GL_ARRAY_BUFFER, sizeof (corners), corners, GL_STATIC_DRAW);
  508. glEnableVertexAttribArray (cornerAttribute);
  509. glVertexAttribPointer (cornerAttribute, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
  510. glGenBuffers (1, &this->m_orderBuffer);
  511. glBindBuffer (GL_ARRAY_BUFFER, this->m_orderBuffer);
  512. glEnableVertexAttribArray (indexAttribute);
  513. glVertexAttribIPointer (indexAttribute, 1, GL_UNSIGNED_INT, 0, nullptr);
  514. glVertexAttribDivisor (indexAttribute, 1);
  515. glBindVertexArray (GL_NONE);
  516. glBindBuffer (GL_ARRAY_BUFFER, GL_NONE);
  517. }
  518. void CSplat::resizeOutput (int width, int height) {
  519. this->m_width = width;
  520. this->m_height = height;
  521. this->m_hasBase = false;
  522. this->m_hasDrawn = false;
  523. glBindTexture (GL_TEXTURE_2D, this->getWallpaperTexture ());
  524. glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
  525. glBindTexture (GL_TEXTURE_2D, 0);
  526. }
  527. std::vector<CSplat::ClockPoint> CSplat::buildClockPoints (
  528. const std::string& text, float aspect, float size, float originXPercent, float originYPercent
  529. ) const {
  530. // seven segment layout in a digit's own units: segments a-g as (x1, y1, x2, y2)
  531. static constexpr float SEGMENT_LINES[7][4] = {
  532. { 0.18f, 0.00f, 0.82f, 0.00f }, { 1.00f, 0.14f, 1.00f, 0.68f }, { 1.00f, 0.92f, 1.00f, 1.46f },
  533. { 0.18f, 1.60f, 0.82f, 1.60f }, { 0.00f, 0.92f, 0.00f, 1.46f }, { 0.00f, 0.14f, 0.00f, 0.68f },
  534. { 0.18f, 0.80f, 0.82f, 0.80f },
  535. };
  536. static constexpr const char* DIGIT_SEGMENTS[10]
  537. = { "abcdef", "bc", "abged", "abgcd", "fgbc", "afgcd", "afgecd", "abc", "abcdefg", "abcdfg" };
  538. constexpr float DIGIT_HEIGHT = 1.6f;
  539. constexpr float DIGIT_ADVANCE = 1.32f;
  540. constexpr float COLON_WIDTH = 0.38f;
  541. constexpr float CLOCK_WIDTH = 0.36f;
  542. struct Raw {
  543. float x;
  544. float y;
  545. bool horizontal;
  546. float logicalHalfLength;
  547. };
  548. std::vector<Raw> raw;
  549. float cursor = 0.0f;
  550. for (const char character : text) {
  551. if (character == ':') {
  552. raw.push_back ({ cursor + COLON_WIDTH * 0.5f, 0.54f, false, 0.0f });
  553. raw.push_back ({ cursor + COLON_WIDTH * 0.5f, 1.06f, false, 0.0f });
  554. cursor += COLON_WIDTH + 0.30f;
  555. continue;
  556. }
  557. if (character < '0' || character > '9') {
  558. continue;
  559. }
  560. for (const char* segment = DIGIT_SEGMENTS[character - '0']; *segment != '\0'; segment++) {
  561. const float* line = SEGMENT_LINES[*segment - 'a'];
  562. const float dx = std::abs (line[2] - line[0]);
  563. const float dy = std::abs (line[3] - line[1]);
  564. raw.push_back (
  565. { cursor + (line[0] + line[2]) * 0.5f, (line[1] + line[3]) * 0.5f, dx >= dy, std::max (dx, dy) * 0.5f }
  566. );
  567. }
  568. cursor += DIGIT_ADVANCE;
  569. }
  570. const float totalWidth = std::max (cursor - 0.16f, 1.0f);
  571. const float width = CLOCK_WIDTH * size;
  572. const float height = width * (DIGIT_HEIGHT / totalWidth) * aspect;
  573. const float originX = std::clamp (originXPercent / 100.0f, 0.02f, 0.98f);
  574. const float originY = std::clamp (originYPercent / 100.0f, 0.02f, 0.98f);
  575. std::vector<ClockPoint> points;
  576. for (size_t i = 0; i < raw.size () && i < CLOCK_POINT_CAPACITY; i++) {
  577. const Raw& point = raw[i];
  578. ClockPoint result;
  579. result.x = std::clamp (originX + (point.x / totalWidth - 0.5f) * width, 0.0f, 1.0f);
  580. result.y = std::clamp (originY + (point.y / DIGIT_HEIGHT - 0.5f) * height, 0.0f, 1.0f);
  581. if (point.logicalHalfLength > 0.0f) {
  582. result.halfLength = point.horizontal ? point.logicalHalfLength / totalWidth * width * aspect
  583. : -point.logicalHalfLength / DIGIT_HEIGHT * height;
  584. }
  585. result.strength = 1.0f;
  586. points.push_back (result);
  587. }
  588. return points;
  589. }
  590. bool CSplat::updateClock (float aspect) {
  591. constexpr float TRANSITION_SECONDS = 0.82f;
  592. constexpr float BASE_RADIUS = 0.011f;
  593. const bool enabled = this->numberProperty ("clockEnabled", 1.0) != 0.0;
  594. const float size = std::clamp (static_cast<float> (this->numberProperty ("clockSize", 1.0)), 0.55f, 1.8f);
  595. const float x = std::clamp (static_cast<float> (this->numberProperty ("clockX", 50.0)), 8.0f, 92.0f);
  596. const float y = std::clamp (static_cast<float> (this->numberProperty ("clockY", 18.0)), 8.0f, 92.0f);
  597. const float lift = std::clamp (static_cast<float> (this->numberProperty ("clockLift", 0.92)), 0.0f, 2.0f);
  598. const float distance = std::clamp (static_cast<float> (this->numberProperty ("clockDistance", 0.92)), 0.0f, 2.0f);
  599. const float radius = std::clamp (BASE_RADIUS * size, 0.008f, 0.055f);
  600. const auto now = std::chrono::steady_clock::now ();
  601. const std::array<float, 5> layout = { size, x, y, aspect, enabled ? 1.0f : 0.0f };
  602. const std::time_t wallClock = std::time (nullptr);
  603. std::tm local {};
  604. localtime_r (&wallClock, &local);
  605. char text[8];
  606. std::snprintf (text, sizeof (text), "%02d:%02d", local.tm_hour, local.tm_min);
  607. if (layout != this->m_clockLayout) {
  608. // first frame, or the clock was moved/resized: rebuild it from nothing so it rises in again
  609. this->m_clockLayout = layout;
  610. this->m_clockText = text;
  611. this->m_clockPrevious.clear ();
  612. this->m_clockCurrent = this->buildClockPoints (this->m_clockText, aspect, size, x, y);
  613. this->m_clockTransitionStart = now;
  614. } else if (this->m_clockText != text || this->m_clockCurrent.empty ()) {
  615. this->m_clockPrevious = this->m_clockCurrent;
  616. this->m_clockText = text;
  617. this->m_clockCurrent = this->buildClockPoints (this->m_clockText, aspect, size, x, y);
  618. this->m_clockTransitionStart = now;
  619. }
  620. const float progress = std::clamp (std::chrono::duration<float> (now - this->m_clockTransitionStart).count () / TRANSITION_SECONDS, 0.0f, 1.0f);
  621. const float rise = 1.0f - std::pow (1.0f - progress, 3.0f);
  622. const float sink = std::pow (1.0f - progress, 3.0f);
  623. // points that stay across a minute change keep full strength; new ones rise, old ones sink away
  624. const auto keyOf = [] (const ClockPoint& point) {
  625. return std::make_pair (std::lround (point.x * 10000.0f), std::lround (point.y * 10000.0f));
  626. };
  627. std::vector<std::pair<long, long>> currentKeys;
  628. std::vector<std::pair<long, long>> previousKeys;
  629. for (const auto& point : this->m_clockCurrent) {
  630. currentKeys.push_back (keyOf (point));
  631. }
  632. for (const auto& point : this->m_clockPrevious) {
  633. previousKeys.push_back (keyOf (point));
  634. }
  635. const auto contains = [] (const std::vector<std::pair<long, long>>& keys, const std::pair<long, long>& key) {
  636. return std::find (keys.begin (), keys.end (), key) != keys.end ();
  637. };
  638. std::vector<ClockPoint> packed;
  639. for (const auto& point : this->m_clockCurrent) {
  640. ClockPoint entry = point;
  641. entry.strength = contains (previousKeys, keyOf (point)) ? 1.0f : rise;
  642. packed.push_back (entry);
  643. }
  644. if (progress < 1.0f) {
  645. for (const auto& point : this->m_clockPrevious) {
  646. if (!contains (currentKeys, keyOf (point))) {
  647. ClockPoint entry = point;
  648. entry.strength = -sink;
  649. packed.push_back (entry);
  650. }
  651. }
  652. }
  653. std::stable_sort (packed.begin (), packed.end (), [] (const ClockPoint& a, const ClockPoint& b) {
  654. return std::abs (a.strength) > std::abs (b.strength);
  655. });
  656. std::array<glm::vec4, CLOCK_POINT_CAPACITY> points = {};
  657. bool active = false;
  658. glm::vec4 bounds (1.0f, 1.0f, 0.0f, 0.0f);
  659. // where the digits end up floating to: the splats behind that spot get faded out
  660. const float finalRise = radius * (0.58f + 2.35f) * lift;
  661. for (size_t i = 0; i < packed.size () && i < CLOCK_POINT_CAPACITY; i++) {
  662. const ClockPoint& point = packed[i];
  663. const float strength = enabled ? std::clamp (point.strength, -1.0f, 1.0f) : 0.0f;
  664. points[i] = { point.x, point.y, strength, point.halfLength };
  665. if (std::abs (strength) <= 0.001f) {
  666. continue;
  667. }
  668. active = true;
  669. // reach of this capsule on screen (x is stretched by the aspect ratio when measuring distance)
  670. const float halfLength = std::abs (point.halfLength);
  671. const float reachX = ((point.halfLength > 0.0f ? halfLength : 0.0f) + radius) / std::max (1.0f, aspect);
  672. const float reachY = (point.halfLength < 0.0f ? halfLength : 0.0f) + radius;
  673. bounds.x = std::min (bounds.x, point.x - reachX);
  674. bounds.y = std::min (bounds.y, point.y - reachY - finalRise);
  675. bounds.z = std::max (bounds.z, point.x + reachX);
  676. bounds.w = std::max (bounds.w, point.y + reachY);
  677. }
  678. const glm::vec4 params (active ? 1.0f : 0.0f, lift, radius, std::max (1.0f, aspect));
  679. const bool changed = points != this->m_clockPoints || params != this->m_clockParams || distance != this->m_clockDistance;
  680. this->m_clockPoints = points;
  681. this->m_clockParams = params;
  682. this->m_clockBounds = bounds;
  683. this->m_clockDistance = distance;
  684. return changed;
  685. }
  686. CSplat::BaseCamera CSplat::buildBaseCamera (int width, int height, float focusDepth) const {
  687. const SceneProfile& profile = this->m_profile;
  688. BaseCamera base;
  689. // the viewer treats a focus depth of 1 or less as "unset" and falls back to 30%
  690. const float focusPercent = std::clamp (focusDepth <= 1.0f ? 30.0f : focusDepth, 0.0f, 100.0f) / 100.0f;
  691. const float focusOnAxis = profile.depthMin + (profile.depthMax - profile.depthMin) * focusPercent;
  692. const float focusDistance = std::max (0.01f, focusOnAxis / std::max (profile.direction.z, 1e-4f));
  693. base.position = glm::vec3 (0.0f);
  694. base.target = profile.direction * focusDistance;
  695. base.distance = focusDistance;
  696. base.front = glm::normalize (base.position - base.target);
  697. base.right = glm::normalize (glm::cross (glm::vec3 (0.0f, 1.0f, 0.0f), profile.direction));
  698. base.up = glm::normalize (glm::cross (profile.direction, base.right));
  699. const float inverseFocus = 1.0f / std::max (focusOnAxis, 1e-4f);
  700. base.parallaxDepthFactor = std::max (
  701. std::abs (1.0f / std::max (profile.depthMin, 1e-4f) - inverseFocus),
  702. std::abs (1.0f / std::max (profile.depthMax, 1e-4f) - inverseFocus)
  703. );
  704. const float cameraSide = base.front.z < 0.0f ? -1.0f : 1.0f;
  705. base.frontZ = profile.boundsCenter.z + cameraSide * profile.boundsHalfExtents.z;
  706. // Fit the photo's angular size to the viewport. The base page crops to fill (the smaller of the
  707. // two fits); presets show the whole photo (the larger). Both keep the viewer's 4% breathing room.
  708. const float aspect = static_cast<float> (width) / static_cast<float> (std::max (height, 1));
  709. const float horizontal = profile.angularSize.x;
  710. const float vertical = profile.angularSize.y;
  711. const float horizontalFit = width > height ? horizontal : 2.0f * std::atan (std::tan (horizontal * 0.5f) / aspect);
  712. const float verticalFit = width > height ? 2.0f * std::atan (std::tan (vertical * 0.5f) * aspect) : vertical;
  713. const float fov = this->m_containFraming ? std::max (horizontalFit, verticalFit) : std::min (horizontalFit, verticalFit);
  714. base.fov = std::clamp (glm::degrees (fov) * 1.04f, 20.0f, 120.0f);
  715. base.viewport = { width, height };
  716. base.focusDepth = focusDepth;
  717. return base;
  718. }
  719. bool CSplat::viewFitsFrontFace (const glm::vec3& position, const glm::vec3& target, const BaseCamera& base) const {
  720. const glm::vec3 toTarget = target - position;
  721. if (glm::length (toTarget) <= 1e-6f) {
  722. return false;
  723. }
  724. const glm::vec3 forward = glm::normalize (toTarget);
  725. const glm::vec3 sideways (forward.z, 0.0f, -forward.x);
  726. if (glm::length (sideways) <= 1e-6f) {
  727. return false;
  728. }
  729. const glm::vec3 right = glm::normalize (sideways);
  730. const glm::vec3 up = glm::normalize (glm::cross (forward, right));
  731. const glm::vec2 tangents = frustumTangents (base.fov, base.viewport.x, base.viewport.y);
  732. const glm::vec3& center = this->m_profile.boundsCenter;
  733. const glm::vec3& half = this->m_profile.boundsHalfExtents;
  734. const float epsilon = std::max (half.x, half.y) * 0.0001f;
  735. for (const float xSign : { -1.0f, 1.0f }) {
  736. for (const float ySign : { -1.0f, 1.0f }) {
  737. const glm::vec3 ray = forward + right * tangents.x * xSign + up * tangents.y * ySign;
  738. if (std::abs (ray.z) <= 1e-6f) {
  739. return false;
  740. }
  741. const float distance = (base.frontZ - position.z) / ray.z;
  742. if (!std::isfinite (distance) || distance <= 0.0f) {
  743. return false;
  744. }
  745. const glm::vec3 hit = position + ray * distance;
  746. if (hit.x < center.x - half.x - epsilon || hit.x > center.x + half.x + epsilon
  747. || hit.y < center.y - half.y - epsilon || hit.y > center.y + half.y + epsilon) {
  748. return false;
  749. }
  750. }
  751. }
  752. return true;
  753. }
  754. // The viewer's camera: sits at the photo's origin and moves on a sphere around the focus point,
  755. // as far as the mouse asks without the view leaving the front face of the splat cloud.
  756. CSplat::Pose CSplat::posePlacement (
  757. const BaseCamera& base, glm::vec2 tilt, glm::vec2 orbit, float parallaxStrength
  758. ) const {
  759. const float strength = std::clamp (parallaxStrength, 0.0f, 0.16f);
  760. float parallaxMove = base.distance * strength * 2.4f;
  761. if (std::isfinite (base.parallaxDepthFactor) && base.parallaxDepthFactor > 1e-6f) {
  762. const glm::vec2 tangents = frustumTangents (base.fov, base.viewport.x, base.viewport.y);
  763. const float screenTangent = std::max (0.001f, std::min (tangents.x, tangents.y));
  764. const float move = strength * 2.0f * screenTangent / base.parallaxDepthFactor;
  765. if (std::isfinite (move) && move > 0.0f) {
  766. parallaxMove = std::min (move, base.distance * 0.8f);
  767. }
  768. }
  769. const float orbitMove = std::min (std::max (base.distance, 1e-4f) * 0.18f, base.distance * 0.8f);
  770. const glm::vec3 mouseOffset = base.right * (tilt.x * parallaxMove) + base.up * (-tilt.y * parallaxMove);
  771. const glm::vec3 orbitOffset = base.right * (orbit.x * orbitMove) + base.up * (-orbit.y * orbitMove);
  772. const auto fits = [&] (const glm::vec3& offset) -> std::optional<glm::vec3> {
  773. const float tangentDistance = glm::length (offset);
  774. if (tangentDistance >= base.distance) {
  775. return std::nullopt;
  776. }
  777. const float frontDistance = std::sqrt (std::max (base.distance * base.distance - tangentDistance * tangentDistance, 0.0f));
  778. const glm::vec3 position = base.target + base.front * frontDistance + offset;
  779. if (!this->viewFitsFrontFace (position, base.target, base)) {
  780. return std::nullopt;
  781. }
  782. return position;
  783. };
  784. // shrink an offset toward zero until the view fits, 14 halvings like the viewer
  785. const auto shrink = [&] (const glm::vec3& fixed, const glm::vec3& variable, glm::vec3 position) {
  786. float low = 0.0f;
  787. float high = 1.0f;
  788. for (int iteration = 0; iteration < 14; iteration++) {
  789. const float middle = (low + high) * 0.5f;
  790. if (const auto candidate = fits (fixed + variable * middle)) {
  791. low = middle;
  792. position = *candidate;
  793. } else {
  794. high = middle;
  795. }
  796. }
  797. return position;
  798. };
  799. Pose pose;
  800. pose.target = base.target;
  801. if (const auto full = fits (orbitOffset + mouseOffset)) {
  802. pose.position = *full;
  803. } else if (const auto orbitOnly = fits (orbitOffset)) {
  804. pose.position = shrink (orbitOffset, mouseOffset, *orbitOnly);
  805. } else {
  806. pose.position = base.position;
  807. if (this->viewFitsFrontFace (pose.position, base.target, base)) {
  808. pose.position = shrink (glm::vec3 (0.0f), orbitOffset, pose.position);
  809. }
  810. }
  811. return pose;
  812. }
  813. void CSplat::renderFrame (const glm::ivec4& viewport) {
  814. if (viewport.z <= 0 || viewport.w <= 0) {
  815. return;
  816. }
  817. if (viewport.z != this->m_width || viewport.w != this->m_height) {
  818. this->resizeOutput (viewport.z, viewport.w);
  819. }
  820. const bool depthEnabled = this->numberProperty ("depthEnabled", 1.0) != 0.0;
  821. const float focusDepth = static_cast<float> (this->numberProperty ("focusDepth", 30.0));
  822. if (!this->m_hasBase || this->m_base.viewport != glm::ivec2 (this->m_width, this->m_height)
  823. || this->m_base.focusDepth != focusDepth) {
  824. this->m_base = this->buildBaseCamera (this->m_width, this->m_height, focusDepth);
  825. this->m_hasBase = true;
  826. }
  827. const auto now = std::chrono::steady_clock::now ();
  828. const float elapsed = this->m_hasLastFrame ? std::chrono::duration<float> (now - this->m_lastFrame).count () : 0.0f;
  829. const float delta = std::clamp (elapsed, 0.0f, 0.05f);
  830. this->m_lastFrame = now;
  831. this->m_hasLastFrame = true;
  832. // mouse -> target tilt, -1..1 with y up. Skipped for --disable-parallax, and until the wallpaper's
  833. // depth effect is switched on.
  834. glm::vec2 targetTilt (0.0f);
  835. const auto& settings = this->getContext ().getApp ().getContext ().settings;
  836. if (depthEnabled && !settings.mouse.disableparallax) {
  837. const glm::dvec2 mouse = this->getContext ().getInputContext ().getMouseInput ().position ();
  838. const float sensitivity = static_cast<float> (this->numberProperty ("sensorSensitivity", 4.0)) / 4.0f;
  839. const float normalizedX = std::clamp (static_cast<float> ((mouse.x - viewport.x) / viewport.z), 0.0f, 1.0f);
  840. const float normalizedY = std::clamp (static_cast<float> ((mouse.y - viewport.y) / viewport.w), 0.0f, 1.0f);
  841. targetTilt = glm::clamp (glm::vec2 (normalizedX * 2.0f - 1.0f, normalizedY * 2.0f - 1.0f) * sensitivity, -1.0f, 1.0f);
  842. }
  843. // the viewer eases toward the target by 14% per 60Hz frame
  844. const float inertia = 1.0f - std::pow (1.0f - 0.14f, delta * 60.0f);
  845. this->m_tilt += (targetTilt - this->m_tilt) * inertia;
  846. for (int axis = 0; axis < 2; axis++) {
  847. if (std::abs (targetTilt[axis] - this->m_tilt[axis]) < 0.0005f) {
  848. this->m_tilt[axis] = targetTilt[axis];
  849. }
  850. }
  851. glm::vec2 tilt = this->m_tilt;
  852. const float tiltLength = glm::length (tilt);
  853. if (tiltLength > 0.55f) {
  854. tilt *= 0.55f / tiltLength;
  855. }
  856. glm::vec2 orbit (0.0f);
  857. if (depthEnabled && this->numberProperty ("orbitEnabled", 0.0) != 0.0) {
  858. const float speed = static_cast<float> (this->numberProperty ("orbitSpeed", 0.12));
  859. const float amount = static_cast<float> (this->numberProperty ("orbitAmount", 0.34));
  860. this->m_orbitPhase = std::fmod (this->m_orbitPhase + delta * speed * glm::two_pi<float> (), glm::two_pi<float> ());
  861. orbit = { std::cos (this->m_orbitPhase) * amount, std::sin (this->m_orbitPhase) * amount * 0.47f };
  862. }
  863. const float parallaxStrength = static_cast<float> (this->numberProperty ("parallaxStrength", 0.08));
  864. const Pose pose = this->posePlacement (this->m_base, tilt, orbit, parallaxStrength);
  865. const glm::vec3 forward = glm::normalize (pose.target - pose.position);
  866. // The splat data stays in the photo's frame, so the world flip the viewer applies to its entity
  867. // goes into the view matrix instead
  868. const glm::mat4 flip = glm::scale (glm::mat4 (1.0f), glm::vec3 (-1.0f, -1.0f, 1.0f));
  869. const glm::mat4 view = glm::lookAt (pose.position, pose.target, glm::vec3 (0.0f, 1.0f, 0.0f)) * flip;
  870. const glm::vec2 tangents = frustumTangents (this->m_base.fov, this->m_width, this->m_height);
  871. constexpr float nearPlane = 0.05f;
  872. constexpr float farPlane = 200.0f;
  873. glm::mat4 projection (0.0f);
  874. projection[0][0] = 1.0f / tangents.x;
  875. projection[1][1] = 1.0f / tangents.y;
  876. projection[2][2] = -(farPlane + nearPlane) / (farPlane - nearPlane);
  877. projection[2][3] = -1.0f;
  878. projection[3][2] = -2.0f * farPlane * nearPlane / (farPlane - nearPlane);
  879. const glm::vec2 focal (
  880. static_cast<float> (this->m_width) * 0.5f / tangents.x, static_cast<float> (this->m_height) * 0.5f / tangents.y
  881. );
  882. // re-sort only once the camera has moved enough to change the ordering visibly
  883. const bool moved = !this->m_hasOrder
  884. || glm::distance (pose.position, this->m_sortedPosition) > this->m_base.distance * 0.0002f
  885. || glm::dot (forward, this->m_sortedForward) < 0.99999f;
  886. std::vector<uint32_t> order;
  887. bool haveNewOrder = false;
  888. if (!this->m_hasOrder) {
  889. // nothing to draw yet: this one has to be synchronous
  890. this->m_sorter->sortNow (pose.position, forward, order);
  891. this->m_hasOrder = true;
  892. haveNewOrder = true;
  893. } else if (this->m_sorter->takeResult (order)) {
  894. haveNewOrder = true;
  895. }
  896. if (moved) {
  897. this->m_sorter->request (pose.position, forward);
  898. this->m_sortedPosition = pose.position;
  899. this->m_sortedForward = forward;
  900. }
  901. if (haveNewOrder) {
  902. glBindBuffer (GL_ARRAY_BUFFER, this->m_orderBuffer);
  903. glBufferData (
  904. GL_ARRAY_BUFFER, static_cast<GLsizeiptr> (order.size () * sizeof (uint32_t)), order.data (), GL_STREAM_DRAW
  905. );
  906. glBindBuffer (GL_ARRAY_BUFFER, GL_NONE);
  907. this->m_drawCount = static_cast<uint32_t> (order.size ());
  908. }
  909. const bool clockChanged = this->updateClock (static_cast<float> (this->m_width) / static_cast<float> (this->m_height));
  910. // a still camera and clock produce the exact same frame, which is still sitting in the output texture
  911. if (this->m_hasDrawn && !haveNewOrder && !clockChanged && view == this->m_drawnView
  912. && projection == this->m_drawnProjection) {
  913. return;
  914. }
  915. this->m_drawnView = view;
  916. this->m_drawnProjection = projection;
  917. this->m_hasDrawn = true;
  918. glBindFramebuffer (GL_FRAMEBUFFER, this->getWallpaperFramebuffer ());
  919. glViewport (0, 0, this->m_width, this->m_height);
  920. glClearColor (0.0f, 0.0f, 0.0f, 1.0f);
  921. glClear (GL_COLOR_BUFFER_BIT);
  922. if (this->m_drawCount == 0) {
  923. return;
  924. }
  925. glDisable (GL_DEPTH_TEST);
  926. glDisable (GL_CULL_FACE);
  927. glEnable (GL_BLEND);
  928. // splats arrive back to front; keep the cleared alpha so the finished frame stays opaque
  929. glBlendFuncSeparate (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ZERO, GL_ONE);
  930. glUseProgram (this->m_program);
  931. glActiveTexture (GL_TEXTURE0);
  932. glBindTexture (GL_TEXTURE_2D, this->m_centerTexture);
  933. glActiveTexture (GL_TEXTURE1);
  934. glBindTexture (GL_TEXTURE_2D, this->m_rotationTexture);
  935. glActiveTexture (GL_TEXTURE2);
  936. glBindTexture (GL_TEXTURE_2D, this->m_scaleTexture);
  937. glUniform1i (this->u_Centers, 0);
  938. glUniform1i (this->u_Rotations, 1);
  939. glUniform1i (this->u_Scales, 2);
  940. glUniformMatrix4fv (this->u_View, 1, GL_FALSE, glm::value_ptr (view));
  941. glUniformMatrix4fv (this->u_Projection, 1, GL_FALSE, glm::value_ptr (projection));
  942. glUniform2f (this->u_Viewport, static_cast<float> (this->m_width), static_cast<float> (this->m_height));
  943. glUniform2f (this->u_Focal, focal.x, focal.y);
  944. glUniform1i (this->u_TextureWidth, static_cast<GLint> (this->m_textureWidth));
  945. glUniform4fv (this->u_ClockPoints, static_cast<GLsizei> (CLOCK_POINT_CAPACITY), glm::value_ptr (this->m_clockPoints[0]));
  946. glUniform4fv (this->u_ClockParams, 1, glm::value_ptr (this->m_clockParams));
  947. glUniform4fv (this->u_ClockBounds, 1, glm::value_ptr (this->m_clockBounds));
  948. glUniform1f (this->u_ClockDistance, this->m_clockDistance);
  949. glBindVertexArray (this->m_vao);
  950. glDrawArraysInstanced (GL_TRIANGLE_STRIP, 0, 4, static_cast<GLsizei> (this->m_drawCount));
  951. glBindVertexArray (GL_NONE);
  952. glDisable (GL_BLEND);
  953. glActiveTexture (GL_TEXTURE0);
  954. }