File indexing completed on 2024-04-21 03:44:45
0001 /* 0002 SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "terrainrenderer.h" 0008 0009 #include "kstars_debug.h" 0010 #include "Options.h" 0011 #include "skymap.h" 0012 #include "skyqpainter.h" 0013 #include "projections/projector.h" 0014 #include "projections/equirectangularprojector.h" 0015 #include "skypoint.h" 0016 #include "kstars.h" 0017 0018 #include <QStatusBar> 0019 0020 // This is the factory that builds the one-and-only TerrainRenderer. 0021 TerrainRenderer * TerrainRenderer::_terrainRenderer = nullptr; 0022 TerrainRenderer *TerrainRenderer::Instance() 0023 { 0024 if (_terrainRenderer == nullptr) 0025 _terrainRenderer = new TerrainRenderer(); 0026 0027 return _terrainRenderer; 0028 } 0029 0030 // This class implements a quick 2D float array. 0031 class TerrainLookup 0032 { 0033 public: 0034 TerrainLookup(int width, int height) : 0035 valPtr(new float[width * height]), valWidth(width) 0036 { 0037 memset(valPtr, 0, width * height * sizeof(float)); 0038 } 0039 ~TerrainLookup() 0040 { 0041 delete[] valPtr; 0042 } 0043 inline float get(int w, int h) 0044 { 0045 return valPtr[h * valWidth + w]; 0046 } 0047 inline void set(int w, int h, float val) 0048 { 0049 valPtr[h * valWidth + w] = val; 0050 } 0051 private: 0052 float *valPtr; 0053 int valWidth = 0; 0054 }; 0055 0056 // Samples 2-D array and returns interpolated values for the unsampled elements. 0057 // Used to speed up the calculation of azimuth and altitude values for all pixels 0058 // of the array to be rendered. Instead we compute every nth pixel's coordinates (e.g. n=4) 0059 // and interpolate the azimuth and alitutde values (so 1/16 the number of calculations). 0060 // This should be more than accurate enough for this rendering, and this part 0061 // of the code definitely needs speed. 0062 class InterpArray 0063 { 0064 public: 0065 // Constructor calculates the downsampled size and allocates the 2D arrays 0066 // for azimuth and altitude. 0067 InterpArray(int width, int height, int samplingFactor) : sampling(samplingFactor) 0068 { 0069 int downsampledWidth = width / sampling; 0070 if (width % sampling != 0) 0071 downsampledWidth += 1; 0072 lastDownsampledCol = downsampledWidth - 1; 0073 0074 int downsampledHeight = height / sampling; 0075 if (height % sampling != 0) 0076 downsampledHeight += 1; 0077 lastDownsampledRow = downsampledHeight - 1; 0078 0079 azLookup = new TerrainLookup(downsampledWidth, downsampledHeight); 0080 altLookup = new TerrainLookup(downsampledWidth, downsampledHeight); 0081 } 0082 0083 ~InterpArray() 0084 { 0085 delete azLookup; 0086 delete altLookup; 0087 } 0088 0089 // Get the azimuth and altitude values from the 2D arrays. 0090 // Inputs are a full-image position 0091 inline void get(int x, int y, float *az, float *alt) 0092 { 0093 const bool rowSampled = y % sampling == 0; 0094 const bool colSampled = x % sampling == 0; 0095 const int xSampled = x / sampling; 0096 const int ySampled = y / sampling; 0097 0098 // If the requested position has been calculated, the use the stored value. 0099 if (rowSampled && colSampled) 0100 { 0101 *az = azLookup->get(xSampled, ySampled); 0102 *alt = altLookup->get(xSampled, ySampled); 0103 return; 0104 } 0105 // The desired position has not been calculated, but values on its row have been calculated. 0106 // Interpolate between the nearest values on the row. 0107 else if (rowSampled) 0108 { 0109 if (xSampled >= lastDownsampledCol) 0110 { 0111 // If the requested value is on or past the last calculated value, 0112 // just use that last value. 0113 *az = azLookup->get(xSampled, ySampled); 0114 *alt = altLookup->get(xSampled, ySampled); 0115 return; 0116 } 0117 // Weight the contributions of the 2 calculated values on the row by their distance to the desired position. 0118 const float weight2 = static_cast<float>(x) / sampling - xSampled; 0119 const float weight1 = 1 - weight2; 0120 *az = weight1 * azLookup->get(xSampled, ySampled) + weight2 * azLookup->get(xSampled + 1, ySampled); 0121 *alt = weight1 * altLookup->get(xSampled, ySampled) + weight2 * altLookup->get(xSampled + 1, ySampled); 0122 return; 0123 } 0124 // The desired position has not been calculated, but values on its column have been calculated. 0125 // Interpolate between the nearest values on the column. 0126 else if (colSampled) 0127 { 0128 if (ySampled >= lastDownsampledRow) 0129 { 0130 // If the requested value is on or past the last calculated value, 0131 // just use that last value. 0132 *az = azLookup->get(xSampled, ySampled); 0133 *alt = altLookup->get(xSampled, ySampled); 0134 return; 0135 } 0136 // Weight the contributions of the 2 calculated values on the column by their distance to the desired position. 0137 const float weight2 = static_cast<float>(y) / sampling - ySampled; 0138 const float weight1 = 1 - weight2; 0139 *az = weight1 * azLookup->get(xSampled, ySampled) + weight2 * azLookup->get(xSampled, ySampled + 1); 0140 *alt = weight1 * altLookup->get(xSampled, ySampled) + weight2 * altLookup->get(xSampled, ySampled + 1); 0141 return; 0142 } 0143 else 0144 { 0145 // The desired position has not been calculated, and no values on its column nor row have been calculated. 0146 // Interpolate between the nearest 4 values. 0147 if (xSampled >= lastDownsampledCol || ySampled >= lastDownsampledRow) 0148 { 0149 // If we're near the edge, just use a close value. Harder to interpolate and not too important. 0150 *az = azLookup->get(xSampled, ySampled); 0151 *alt = altLookup->get(xSampled, ySampled); 0152 return; 0153 } 0154 // The section uses distance along the two nearest calculated rows to come up with an estimate. 0155 const float weight2 = static_cast<float>(x) / sampling - xSampled; 0156 const float weight1 = 1 - weight2; 0157 const float azWval = (weight1 * ( azLookup->get(xSampled, ySampled) + azLookup->get(xSampled, ySampled + 1)) + 0158 weight2 * ( azLookup->get(xSampled + 1, ySampled) + azLookup->get(xSampled + 1, ySampled + 1))); 0159 const float altWval = (weight1 * (altLookup->get(xSampled, ySampled) + altLookup->get(xSampled, ySampled + 1)) + 0160 weight2 * (altLookup->get(xSampled + 1, ySampled) + altLookup->get(xSampled + 1, ySampled + 1))); 0161 0162 // The section uses distance along the two nearest calculated columns to come up with an estimate. 0163 const float hweight2 = static_cast<float>(y) / sampling - ySampled; 0164 const float hweight1 = 1 - hweight2; 0165 const float azHval = (hweight2 * ( azLookup->get(xSampled, ySampled) + azLookup->get(xSampled + 1, ySampled)) + 0166 hweight1 * ( azLookup->get(xSampled, ySampled + 1) + azLookup->get(xSampled + 1, ySampled + 1))); 0167 const float altHval = (hweight2 * (altLookup->get(xSampled, ySampled) + altLookup->get(xSampled + 1, ySampled)) + 0168 hweight1 * (altLookup->get(xSampled, ySampled + 1) + altLookup->get(xSampled + 1, ySampled + 1))); 0169 // We average the 2 estimates (note above actually estimated twice the value). 0170 *az = (azWval + azHval) / 4.0; 0171 *alt = (altWval + altHval) / 4.0; 0172 return; 0173 } 0174 } 0175 TerrainLookup *azimuthLookup() 0176 { 0177 return azLookup; 0178 } 0179 TerrainLookup *altitudeLookup() 0180 { 0181 return altLookup; 0182 } 0183 0184 private: 0185 // These are the indeces of the last rows and columns (in the downsampled sized space) that were filled. 0186 // This is needed because the downsample factor might not be an even multiple of the image size. 0187 int lastDownsampledCol = 0; 0188 int lastDownsampledRow = 0; 0189 // The downsample factor. 0190 int sampling = 0; 0191 // The azimuth and altitude values are stored in these 2D arrays. 0192 TerrainLookup *azLookup = nullptr; 0193 TerrainLookup *altLookup = nullptr; 0194 }; 0195 0196 TerrainRenderer::TerrainRenderer() 0197 { 0198 } 0199 0200 // Put degrees in the range of 0 -> 359.99999999 0201 double rationalizeAz(double degrees) 0202 { 0203 if (degrees < -1000 || degrees > 1000) 0204 return 0; 0205 0206 while (degrees < 0) 0207 degrees += 360.0; 0208 while (degrees >= 360.0) 0209 degrees -= 360.0; 0210 return degrees; 0211 } 0212 0213 // Checks that degrees in the range of -90 -> 90. 0214 double rationalizeAlt(double degrees) 0215 { 0216 if (degrees > 90.0) 0217 return 90.0; 0218 if (degrees < -90) 0219 return -90; 0220 return degrees; 0221 } 0222 0223 // Assumes the source photosphere has rows which, left-to-right go from AZ=0 to AZ=360 0224 // and columns go from -90 altitude on the bottom to +90 on top. 0225 // Returns the pixel for the desired azimuth and altitude. 0226 QRgb TerrainRenderer::getPixel(double az, double alt) const 0227 { 0228 az = rationalizeAz(az + Options::terrainSourceCorrectAz()); 0229 // This may make alt > 90 (due to a negative sourceCorrectAlt). 0230 // If so, it returns 0, which is a transparent pixel. 0231 alt = alt - Options::terrainSourceCorrectAlt(); 0232 if (az < 0 || az >= 360 || alt < -90 || alt > 90) 0233 return(0); 0234 0235 // shift az to be -180 to 180 0236 if (az > 180) 0237 az = az - 360.0; 0238 const int width = sourceImage.width(); 0239 const int height = sourceImage.height(); 0240 0241 if (!Options::terrainSmoothPixels()) 0242 { 0243 // az=0 should be the middle of the image. 0244 int pixX = width / 2 + (az / 360.0) * width; 0245 if (pixX > width - 1) 0246 pixX = width - 1; 0247 else if (pixX < 0) 0248 pixX = 0; 0249 int pixY = ((alt + 90.0) / 180.0) * height; 0250 if (pixY > height - 1) 0251 pixY = height - 1; 0252 pixY = (height - 1) - pixY; 0253 return sourceImage.pixel(pixX, pixY); 0254 } 0255 0256 // Get floating point pixel positions so we can interpolate. 0257 float pixX = width / 2 + (az / 360.0) * width; 0258 if (pixX > width - 1) 0259 pixX = width - 1; 0260 else if (pixX < 0) 0261 pixX = 0; 0262 float pixY = ((alt + 90.0) / 180.0) * height; 0263 if (pixY > height - 1) 0264 pixY = height - 1; 0265 pixY = (height - 1) - pixY; 0266 0267 // Don't bother interpolating for transparent pixels. 0268 constexpr int lowAlpha = 0.1 * 255; 0269 if (qAlpha(sourceImage.pixel(pixX, pixY)) < lowAlpha) 0270 return sourceImage.pixel(pixX, pixY); 0271 0272 // Instead of just returning the pixel at the truncated position as above, 0273 // below we interpolate the pixel RGBA values based on the floating-point pixel position. 0274 int x1 = static_cast<int>(pixX); 0275 int y1 = static_cast<int>(pixY); 0276 0277 if ((x1 >= width - 1) || (y1 >= height - 1)) 0278 return sourceImage.pixel(x1, y1); 0279 0280 // weights for the x & x+1, and y & y+1 positions. 0281 float wx2 = pixX - x1; 0282 float wx1 = 1.0 - wx2; 0283 float wy2 = pixY - y1; 0284 float wy1 = 1.0 - wy2; 0285 0286 // The pixels we'll interpolate. 0287 QRgb c11(qUnpremultiply(sourceImage.pixel(pixX, pixY))); 0288 QRgb c12(qUnpremultiply(sourceImage.pixel(pixX, pixY + 1))); 0289 QRgb c21(qUnpremultiply(sourceImage.pixel(pixX + 1, pixY))); 0290 QRgb c22(qUnpremultiply(sourceImage.pixel(pixX + 1, pixY + 1))); 0291 0292 // Weights for the above pixels. 0293 float w11 = wx1 * wy1; 0294 float w12 = wx1 * wy2; 0295 float w21 = wx2 * wy1; 0296 float w22 = wx2 * wy2; 0297 0298 // Finally, interpolate each component. 0299 int red = w11 * qRed(c11) + w12 * qRed(c12) + w21 * qRed(c21) + w22 * qRed(c22); 0300 int green = w11 * qGreen(c11) + w12 * qGreen(c12) + w21 * qGreen(c21) + w22 * qGreen(c22); 0301 int blue = w11 * qBlue(c11) + w12 * qBlue(c12) + w21 * qBlue(c21) + w22 * qBlue(c22); 0302 int alpha = w11 * qAlpha(c11) + w12 * qAlpha(c12) + w21 * qAlpha(c21) + w22 * qAlpha(c22); 0303 return qPremultiply(qRgba(red, green, blue, alpha)); 0304 } 0305 0306 // Checks to see if the view is the same as the last call to render. 0307 // If true, render (below) will skip its computations and return the same image 0308 // as was previously calculated. 0309 // If the view is not the same, this method stores away the details of the current view 0310 // so that it may make this comparison again in the future. 0311 bool TerrainRenderer::sameView(const Projector *proj, bool forceRefresh) 0312 { 0313 ViewParams view = proj->viewParams(); 0314 SkyPoint point = *(view.focus); 0315 const auto &lst = KStarsData::Instance()->lst(); 0316 const auto &lat = KStarsData::Instance()->geo()->lat(); 0317 0318 // We compare az and alt, not RA and DEC, as the RA changes, 0319 // but the rendering is tied to azimuth and altitude which may not change. 0320 // Thus we convert point to horizontal coordinates. 0321 point.EquatorialToHorizontal(lst, lat); 0322 const double az = rationalizeAz(point.az().Degrees()); 0323 const double alt = rationalizeAlt(point.alt().Degrees()); 0324 0325 bool ok = view.width == savedViewParams.width && 0326 view.height == savedViewParams.height && 0327 view.zoomFactor == savedViewParams.zoomFactor && 0328 view.rotationAngle == savedViewParams.rotationAngle && 0329 view.useRefraction == savedViewParams.useRefraction && 0330 view.useAltAz == savedViewParams.useAltAz && 0331 view.fillGround == savedViewParams.fillGround; 0332 const double azDiff = fabs(savedAz - az); 0333 const double altDiff = fabs(savedAlt - alt); 0334 if (!forceRefresh && ok && azDiff < .0001 && altDiff < .0001) 0335 return true; 0336 0337 // Store the view 0338 savedViewParams = view; 0339 savedViewParams.focus = nullptr; 0340 savedAz = az; 0341 savedAlt = alt; 0342 return false; 0343 } 0344 0345 bool TerrainRenderer::render(uint16_t w, uint16_t h, QImage *terrainImage, const Projector *proj) 0346 { 0347 // This is used to force a re-render, e.g. when the image is changed. 0348 bool dirty = false; 0349 0350 // Load the image if necessary. 0351 QString filename = Options::terrainSource(); 0352 if (!initialized || filename != sourceFilename) 0353 { 0354 dirty = true; 0355 QImage image; 0356 if (image.load(filename)) 0357 { 0358 sourceImage = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); 0359 qCDebug(KSTARS) << QString("Read terrain file %1 x %2").arg(sourceImage.width()).arg(sourceImage.height()); 0360 sourceFilename = filename; 0361 initialized = true; 0362 } 0363 else 0364 { 0365 if (filename.isEmpty()) 0366 KStars::Instance()->statusBar()->showMessage(i18n("Failed to load terrain. Set terrain file in Settings.")); 0367 else 0368 KStars::Instance()->statusBar()->showMessage(i18n("Failed to load terrain image (%1). Set terrain file in Settings.", 0369 filename)); 0370 initialized = false; 0371 Options::setShowTerrain(false); 0372 KStars::Instance()->syncOps(); 0373 } 0374 } 0375 0376 if (!initialized) 0377 return false; 0378 0379 // Check if parameters have changed. 0380 if ((terrainDownsampling != Options::terrainDownsampling()) || 0381 (terrainSmoothPixels != Options::terrainSmoothPixels()) || 0382 (terrainSkipSpeedup != Options::terrainSkipSpeedup()) || 0383 (terrainTransparencySpeedup != Options::terrainTransparencySpeedup()) || 0384 (terrainSourceCorrectAz != Options::terrainSourceCorrectAz()) || 0385 (terrainSourceCorrectAlt != Options::terrainSourceCorrectAlt())) 0386 dirty = true; 0387 0388 terrainDownsampling = Options::terrainDownsampling(); 0389 terrainSmoothPixels = Options::terrainSmoothPixels(); 0390 terrainSkipSpeedup = Options::terrainSkipSpeedup(); 0391 terrainTransparencySpeedup = Options::terrainTransparencySpeedup(); 0392 terrainSourceCorrectAz = Options::terrainSourceCorrectAz(); 0393 terrainSourceCorrectAlt = Options::terrainSourceCorrectAlt(); 0394 0395 if (sameView(proj, dirty)) 0396 { 0397 // Just return the previous image if the input view hasn't changed. 0398 *terrainImage = savedImage.copy(); 0399 return true; 0400 } 0401 0402 QElapsedTimer timer; 0403 timer.start(); 0404 0405 // Only compute the pixel's az and alt values for every Nth pixel. 0406 // Get the other pixel az and alt values by interpolation. 0407 // This saves a lot of time. 0408 const int sampling = Options::terrainDownsampling(); 0409 InterpArray interp(w, h, sampling); 0410 QElapsedTimer setupTimer; 0411 setupTimer.start(); 0412 setupLookup(w, h, sampling, proj, interp.azimuthLookup(), interp.altitudeLookup()); 0413 0414 const double setupTime = setupTimer.elapsed() / 1000.0; /////////////////// 0415 0416 // Another speedup. If true, our calculations are downsampled by 2 in each dimension. 0417 const bool skip = Options::terrainSkipSpeedup() || SkyMap::IsSlewing(); 0418 int increment = skip ? 2 : 1; 0419 0420 // Assign transparent pixels everywhere by default. 0421 terrainImage->fill(0); 0422 0423 // Go through the image, and for each pixel, using the previously computed az and alt values 0424 // get the corresponding pixel from the terrain image. 0425 bool lastTransparent = false; 0426 for (int j = 0; j < h; j += increment) 0427 { 0428 bool notLastRow = j != h - 1; 0429 for (int i = 0; i < w; i += increment) 0430 { 0431 if (lastTransparent && Options::terrainTransparencySpeedup()) 0432 { 0433 // Speedup--if the last pixel was transparent, then this 0434 // one is assumed transparent too (but next is calculated). 0435 lastTransparent = false; 0436 continue; 0437 } 0438 0439 const QPointF imgPoint(i, j); 0440 bool equiRectangular = (proj->type() == Projector::Equirectangular); 0441 bool usable = equiRectangular ? !dynamic_cast<const EquirectangularProjector*>(proj)->unusablePoint(imgPoint) 0442 : !proj->unusablePoint(imgPoint); 0443 if (usable) 0444 { 0445 float az, alt; 0446 interp.get(i, j, &az, &alt); 0447 const QRgb pixel = getPixel(az, alt); 0448 terrainImage->setPixel(i, j, pixel); 0449 lastTransparent = (pixel == 0); 0450 0451 if (skip) 0452 { 0453 // If we've skipped, fill in the missing pixels. 0454 bool notLastCol = i != w - 1; 0455 if (notLastCol) 0456 terrainImage->setPixel(i + 1, j, pixel); 0457 if (notLastRow) 0458 terrainImage->setPixel(i, j + 1, pixel); 0459 if (notLastRow && notLastCol) 0460 terrainImage->setPixel(i + 1, j + 1, pixel); 0461 } 0462 } 0463 // Otherwise terrainImage was already filled with transparent pixels 0464 // so i,j will be transparent. 0465 } 0466 } 0467 0468 savedImage = terrainImage->copy(); 0469 0470 QFile f(sourceFilename); 0471 QFileInfo fileInfo(f.fileName()); 0472 QString fName(fileInfo.fileName()); 0473 QString dbgMsg(QString("Terrain rendering: %1px, %2s (%3s) %4 ds %5 skip %6 trnsp %7 pan %8 smooth %9") 0474 .arg(w * h) 0475 .arg(timer.elapsed() / 1000.0, 5, 'f', 3) 0476 .arg(setupTime, 5, 'f', 3) 0477 .arg(fName) 0478 .arg(Options::terrainDownsampling()) 0479 .arg(Options::terrainSkipSpeedup() ? "T" : "F") 0480 .arg(Options::terrainTransparencySpeedup() ? "T" : "F") 0481 .arg(Options::terrainPanning() ? "T" : "F") 0482 .arg(Options::terrainSmoothPixels() ? "T" : "F")); 0483 //qCDebug(KSTARS) << dbgMsg; 0484 //fprintf(stderr, "%s\n", dbgMsg.toLatin1().data()); 0485 0486 dirty = false; 0487 return true; 0488 } 0489 0490 // Goes through every Nth input pixel position, finding their azimuth and altitude 0491 // and storing that for future use in the interpolations above. 0492 // This is the most time-costly part of the computation. 0493 void TerrainRenderer::setupLookup(uint16_t w, uint16_t h, int sampling, const Projector *proj, TerrainLookup *azLookup, 0494 TerrainLookup *altLookup) 0495 { 0496 const auto &lst = KStarsData::Instance()->lst(); 0497 const auto &lat = KStarsData::Instance()->geo()->lat(); 0498 for (int j = 0, js = 0; j < h; j += sampling, js++) 0499 { 0500 for (int i = 0, is = 0; i < w; i += sampling, is++) 0501 { 0502 const QPointF imgPoint(i, j); 0503 bool equiRectangular = (proj->type() == Projector::Equirectangular); 0504 bool usable = equiRectangular ? !dynamic_cast<const EquirectangularProjector*>(proj)->unusablePoint(imgPoint) 0505 : !proj->unusablePoint(imgPoint); 0506 if (usable) 0507 { 0508 SkyPoint point = equiRectangular ? 0509 dynamic_cast<const EquirectangularProjector*>(proj)->fromScreen(imgPoint, lst, lat, true) 0510 : proj->fromScreen(imgPoint, lst, lat, true); 0511 const double az = rationalizeAz(point.az().Degrees()); 0512 const double alt = rationalizeAlt(point.alt().Degrees()); 0513 azLookup->set(is, js, az); 0514 altLookup->set(is, js, alt); 0515 } 0516 } 0517 } 0518 } 0519