File indexing completed on 2024-04-21 03:49:56
0001 // SPDX-License-Identifier: LGPL-2.1-or-later 0002 // 0003 // SPDX-FileCopyrightText: 2007 Torsten Rahn <tackat@kde.org> 0004 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de> 0005 // 0006 0007 0008 #include "SphericalScanlineTextureMapper.h" 0009 0010 #include <cmath> 0011 0012 #include <qmath.h> 0013 #include <QRunnable> 0014 0015 #include "GeoPainter.h" 0016 #include "GeoDataPolygon.h" 0017 #include "MarbleDebug.h" 0018 #include "Quaternion.h" 0019 #include "ScanlineTextureMapperContext.h" 0020 #include "StackedTileLoader.h" 0021 #include "StackedTile.h" 0022 #include "TextureColorizer.h" 0023 #include "ViewportParams.h" 0024 #include "MathHelper.h" 0025 0026 0027 using namespace Marble; 0028 0029 class SphericalScanlineTextureMapper::RenderJob : public QRunnable 0030 { 0031 public: 0032 RenderJob( StackedTileLoader *tileLoader, int tileLevel, QImage *canvasImage, const ViewportParams *viewport, MapQuality mapQuality, int yTop, int yBottom ); 0033 0034 void run() override; 0035 0036 private: 0037 StackedTileLoader *const m_tileLoader; 0038 const int m_tileLevel; 0039 QImage *const m_canvasImage; 0040 const ViewportParams *const m_viewport; 0041 const MapQuality m_mapQuality; 0042 int const m_yTop; 0043 int const m_yBottom; 0044 }; 0045 0046 SphericalScanlineTextureMapper::RenderJob::RenderJob( StackedTileLoader *tileLoader, int tileLevel, QImage *canvasImage, const ViewportParams *viewport, MapQuality mapQuality, int yTop, int yBottom ) 0047 : m_tileLoader( tileLoader ), 0048 m_tileLevel( tileLevel ), 0049 m_canvasImage( canvasImage ), 0050 m_viewport( viewport ), 0051 m_mapQuality( mapQuality ), 0052 m_yTop( yTop ), 0053 m_yBottom( yBottom ) 0054 { 0055 } 0056 0057 SphericalScanlineTextureMapper::SphericalScanlineTextureMapper( StackedTileLoader *tileLoader ) 0058 : TextureMapperInterface() 0059 , m_tileLoader( tileLoader ) 0060 , m_radius( 0 ) 0061 , m_threadPool() 0062 { 0063 } 0064 0065 void SphericalScanlineTextureMapper::mapTexture( GeoPainter *painter, 0066 const ViewportParams *viewport, 0067 int tileZoomLevel, 0068 const QRect &dirtyRect, 0069 TextureColorizer *texColorizer ) 0070 { 0071 if ( m_canvasImage.size() != viewport->size() || m_radius != viewport->radius() ) { 0072 const QImage::Format optimalFormat = ScanlineTextureMapperContext::optimalCanvasImageFormat( viewport ); 0073 0074 if ( m_canvasImage.size() != viewport->size() || m_canvasImage.format() != optimalFormat ) { 0075 m_canvasImage = QImage( viewport->size(), optimalFormat ); 0076 } 0077 0078 if ( !viewport->mapCoversViewport() ) { 0079 m_canvasImage.fill( 0 ); 0080 } 0081 0082 m_radius = viewport->radius(); 0083 m_repaintNeeded = true; 0084 } 0085 0086 if ( m_repaintNeeded ) { 0087 mapTexture( viewport, tileZoomLevel, painter->mapQuality() ); 0088 0089 if ( texColorizer ) { 0090 texColorizer->colorize( &m_canvasImage, viewport, painter->mapQuality() ); 0091 } 0092 0093 m_repaintNeeded = false; 0094 } 0095 0096 const int radius = viewport->radius(); 0097 0098 QRect rect( viewport->width() / 2 - radius, viewport->height() / 2 - radius, 0099 2 * radius, 2 * radius); 0100 rect = rect.intersected( dirtyRect ); 0101 painter->drawImage( rect, m_canvasImage, rect ); 0102 } 0103 0104 void SphericalScanlineTextureMapper::mapTexture( const ViewportParams *viewport, int tileZoomLevel, MapQuality mapQuality ) 0105 { 0106 // Reset backend 0107 m_tileLoader->resetTilehash(); 0108 0109 // Initialize needed constants: 0110 0111 const int imageHeight = m_canvasImage.height(); 0112 const qint64 radius = viewport->radius(); 0113 0114 // Calculate the actual y-range of the map on the screen 0115 const int skip = ( mapQuality == LowQuality ) ? 1 0116 : 0; 0117 const int yTop = ( imageHeight / 2 - radius >= 0 ) ? imageHeight / 2 - radius 0118 : 0; 0119 const int yBottom = ( yTop == 0 ) ? imageHeight - skip 0120 : yTop + radius + radius - skip; 0121 0122 const int numThreads = m_threadPool.maxThreadCount(); 0123 const int yStep = qCeil(qreal( yBottom - yTop ) / qreal(numThreads)); 0124 for ( int i = 0; i < numThreads; ++i ) { 0125 const int yStart = yTop + i * yStep; 0126 const int yEnd = qMin(yBottom, yTop + (i + 1) * yStep); 0127 QRunnable *const job = new RenderJob( m_tileLoader, tileZoomLevel, &m_canvasImage, viewport, mapQuality, yStart, yEnd ); 0128 m_threadPool.start( job ); 0129 } 0130 0131 m_threadPool.waitForDone(); 0132 0133 m_tileLoader->cleanupTilehash(); 0134 } 0135 0136 void SphericalScanlineTextureMapper::RenderJob::run() 0137 { 0138 const int imageHeight = m_canvasImage->height(); 0139 const int imageWidth = m_canvasImage->width(); 0140 const qint64 radius = m_viewport->radius(); 0141 const qreal inverseRadius = 1.0 / (qreal)(radius); 0142 0143 const bool interlaced = ( m_mapQuality == LowQuality ); 0144 const bool highQuality = ( m_mapQuality == HighQuality 0145 || m_mapQuality == PrintQuality ); 0146 const bool printQuality = ( m_mapQuality == PrintQuality ); 0147 0148 // Evaluate the degree of interpolation 0149 const int n = ScanlineTextureMapperContext::interpolationStep( m_viewport, m_mapQuality ); 0150 0151 // Calculate north pole position to decrease pole distortion later on 0152 Quaternion northPole = Quaternion::fromSpherical( 0.0, M_PI * 0.5 ); 0153 northPole.rotateAroundAxis( m_viewport->planetAxis().inverse() ); 0154 const int northPoleX = imageWidth / 2 + (int)( radius * northPole.v[Q_X] ); 0155 const int northPoleY = imageHeight / 2 - (int)( radius * northPole.v[Q_Y] ); 0156 0157 // Calculate axis matrix to represent the planet's rotation. 0158 matrix planetAxisMatrix; 0159 m_viewport->planetAxis().toMatrix( planetAxisMatrix ); 0160 0161 // initialize needed variables that are modified during texture mapping: 0162 0163 ScanlineTextureMapperContext context( m_tileLoader, m_tileLevel ); 0164 qreal lon = 0.0; 0165 qreal lat = 0.0; 0166 0167 // Scanline based algorithm to texture map a sphere 0168 for ( int y = m_yTop; y < m_yBottom ; ++y ) { 0169 0170 // Evaluate coordinates for the 3D position vector of the current pixel 0171 const qreal qy = inverseRadius * (qreal)( imageHeight / 2 - y ); 0172 const qreal qr = 1.0 - qy * qy; 0173 0174 // rx is the radius component in x direction 0175 const int rx = (int)sqrt( (qreal)( radius * radius 0176 - ( ( y - imageHeight / 2 ) 0177 * ( y - imageHeight / 2 ) ) ) ); 0178 0179 // Calculate the actual x-range of the map within the current scanline. 0180 // 0181 // If the circular border of the earth disk is still visible then xLeft 0182 // equals the scanline position of the most left pixel that gets covered 0183 // by the earth disk. In terms of math this equals the half image width minus 0184 // the radius component on the current scanline in x direction ("rx"). 0185 // 0186 // If the zoom factor is high enough then the whole screen gets covered 0187 // by the earth and the border of the earth disk isn't visible anymore. 0188 // In that situation xLeft equals zero. 0189 // For xRight the situation is similar. 0190 0191 const int xLeft = ( imageWidth / 2 - rx > 0 ) ? imageWidth / 2 - rx 0192 : 0; 0193 const int xRight = ( imageWidth / 2 - rx > 0 ) ? xLeft + rx + rx 0194 : imageWidth; 0195 0196 QRgb * scanLine = (QRgb*)( m_canvasImage->scanLine( y ) ) + xLeft; 0197 0198 const int xIpLeft = ( imageWidth / 2 - rx > 0 ) ? n * (int)( xLeft / n + 1 ) 0199 : 1; 0200 const int xIpRight = ( imageWidth / 2 - rx > 0 ) ? n * (int)( xRight / n - 1 ) 0201 : n * (int)( xRight / n - 1 ) + 1; 0202 0203 // Decrease pole distortion due to linear approximation ( y-axis ) 0204 bool crossingPoleArea = false; 0205 if ( northPole.v[Q_Z] > 0 0206 && northPoleY - ( n * 0.75 ) <= y 0207 && northPoleY + ( n * 0.75 ) >= y ) 0208 { 0209 crossingPoleArea = true; 0210 } 0211 0212 int ncount = 0; 0213 0214 for ( int x = xLeft; x < xRight; ++x ) { 0215 // Prepare for interpolation 0216 0217 const int leftInterval = xIpLeft + ncount * n; 0218 0219 bool interpolate = false; 0220 if ( x >= xIpLeft && x <= xIpRight ) { 0221 0222 // Decrease pole distortion due to linear approximation ( x-axis ) 0223 // mDebug() << QString("NorthPole X: %1, LeftInterval: %2").arg( northPoleX ).arg( leftInterval ); 0224 if ( crossingPoleArea 0225 && northPoleX >= leftInterval + n 0226 && northPoleX < leftInterval + 2 * n 0227 && x < leftInterval + 3 * n ) 0228 { 0229 interpolate = false; 0230 } 0231 else { 0232 x += n - 1; 0233 interpolate = !printQuality; 0234 ++ncount; 0235 } 0236 } 0237 else 0238 interpolate = false; 0239 0240 // Evaluate more coordinates for the 3D position vector of 0241 // the current pixel. 0242 const qreal qx = (qreal)( x - imageWidth / 2 ) * inverseRadius; 0243 const qreal qr2z = qr - qx * qx; 0244 const qreal qz = ( qr2z > 0.0 ) ? sqrt( qr2z ) : 0.0; 0245 0246 // Create Quaternion from vector coordinates and rotate it 0247 // around globe axis 0248 Quaternion qpos( 0.0, qx, qy, qz ); 0249 qpos.rotateAroundAxis( planetAxisMatrix ); 0250 0251 qpos.getSpherical( lon, lat ); 0252 // mDebug() << QString("lon: %1 lat: %2").arg(lon).arg(lat); 0253 // Approx for n-1 out of n pixels within the boundary of 0254 // xIpLeft to xIpRight 0255 0256 if ( interpolate ) { 0257 if (highQuality) 0258 context.pixelValueApproxF( lon, lat, scanLine, n ); 0259 else 0260 context.pixelValueApprox( lon, lat, scanLine, n ); 0261 0262 scanLine += ( n - 1 ); 0263 } 0264 0265 // Comment out the pixelValue line and run Marble if you want 0266 // to understand the interpolation: 0267 0268 // Uncomment the crossingPoleArea line to check precise 0269 // rendering around north pole: 0270 0271 // if ( !crossingPoleArea ) 0272 if ( x < imageWidth ) { 0273 if ( highQuality ) 0274 context.pixelValueF( lon, lat, scanLine ); 0275 else 0276 context.pixelValue( lon, lat, scanLine ); 0277 } 0278 0279 ++scanLine; 0280 } 0281 0282 // copy scanline to improve performance 0283 if ( interlaced && y + 1 < m_yBottom ) { 0284 0285 const int pixelByteSize = m_canvasImage->bytesPerLine() / imageWidth; 0286 0287 memcpy( m_canvasImage->scanLine( y + 1 ) + xLeft * pixelByteSize, 0288 m_canvasImage->scanLine( y ) + xLeft * pixelByteSize, 0289 ( xRight - xLeft ) * pixelByteSize ); 0290 ++y; 0291 } 0292 } 0293 }