buildSphere method
Implementation
Future<SphereImage?> buildSphere(double maxWidth, double maxHeight) async {
// Return cached image if still valid
if (_isCacheValid(maxWidth, maxHeight)) {
return _cachedSphereImage;
}
// Prevent concurrent builds
if (_isBuildingSphere) {
return _cachedSphereImage;
}
_isBuildingSphere = true;
if (widget.controller.surface == null ||
widget.controller.surfaceProcessed == null) {
_isBuildingSphere = false;
return Future.value(null);
}
// Check if day/night cycle is enabled and we have night surface
final hasDayNightCycle = widget.controller.isDayNightCycleEnabled &&
widget.controller.nightSurface != null &&
widget.controller.nightSurfaceProcessed != null;
final sphereRadius = convertedRadius().roundToDouble();
final sphereRadiusSquared = sphereRadius * sphereRadius;
final minX = math.max(-sphereRadius, -maxWidth / 2);
final minY = math.max(-sphereRadius, -maxHeight / 2);
final maxX = math.min(sphereRadius, maxWidth / 2);
final maxY = math.min(sphereRadius, maxHeight / 2);
final width = maxX - minX;
final height = maxY - minY;
final widthInt = width.toInt();
final surfaceWidth = widget.controller.surface?.width.toDouble();
final surfaceHeight = widget.controller.surface?.height.toDouble();
final surfaceWidthInt = surfaceWidth!.toInt();
final surfaceHeightInt = surfaceHeight!.toInt();
final spherePixels = Uint32List(widthInt * height.toInt());
// Prepare rotation matrices - combine for efficiency
final rotationMatrixX = Matrix3.rotationX(math.pi / 2 - rotationX);
final rotationMatrixZ = Matrix3.rotationZ(rotationZ + math.pi / 2);
final combinedRotationMatrix = rotationMatrixZ.multiplied(rotationMatrixX);
final surfaceXRate = (surfaceWidth - 1) / (2.0 * math.pi);
final surfaceYRate = (surfaceHeight - 1) / math.pi;
final invSphereRadius = 1.0 / sphereRadius;
// Pre-compute surface data reference for faster access
final surfaceData = widget.controller.surfaceProcessed!;
// Anti-aliasing parameters
const aaWidth = 1.5; // Width of anti-aliasing band in pixels
for (var y = minY; y < maxY; y++) {
final sphereY = (height - y + minY - 1).toInt() * widthInt;
final ySquared = y * y;
for (var x = minX; x < maxX; x++) {
final distSquared = x * x + ySquared;
final dist = math.sqrt(distSquared);
// Calculate edge alpha for anti-aliasing
// Smooth transition from full opacity inside to transparent outside
double edgeAlpha = 1.0;
if (dist > sphereRadius - aaWidth) {
if (dist > sphereRadius + aaWidth * 0.5) {
continue; // Completely outside, skip this pixel
}
// Smooth interpolation at the edge
edgeAlpha = (sphereRadius + aaWidth * 0.5 - dist) / (aaWidth * 1.5);
edgeAlpha = edgeAlpha.clamp(0.0, 1.0);
// Apply smoothstep for better visual quality
edgeAlpha = edgeAlpha * edgeAlpha * (3.0 - 2.0 * edgeAlpha);
}
final zSquared = sphereRadiusSquared - distSquared;
if (zSquared > 0 || edgeAlpha > 0) {
// For edge pixels, use a safe z calculation
final safeZSquared = math.max(
0.0,
sphereRadiusSquared -
math.min(distSquared, sphereRadiusSquared * 0.999));
final z = math.sqrt(safeZSquared);
// For edge pixels, scale position to stay on sphere surface
double effectiveX = x;
double effectiveY = y;
if (dist > sphereRadius * 0.99) {
final scale = sphereRadius * 0.99 / dist;
effectiveX = x * scale;
effectiveY = y * scale;
}
var vector = Vector3(effectiveX, effectiveY, z);
// Apply combined rotation in one step
vector = combinedRotationMatrix.transform(vector);
final lat = math.asin(vector.z * invSphereRadius);
final lon = math.atan2(vector.y, vector.x);
// Invert the x coordinate to fix horizontal mirroring
// This ensures the texture maps correctly (west on left, east on right)
final x0 = (surfaceWidth - 1) - (lon + math.pi) * surfaceXRate;
final y0 = (math.pi / 2 - lat) * surfaceYRate;
// Bilinear interpolation for smoother texture mapping
final x0Floor = x0.floor();
final y0Floor = y0.floor();
final x0Ceil = (x0Floor + 1).clamp(0, surfaceWidthInt - 1);
final y0Ceil = (y0Floor + 1).clamp(0, surfaceHeightInt - 1);
final x0ClampedFloor = x0Floor.clamp(0, surfaceWidthInt - 1);
final y0ClampedFloor = y0Floor.clamp(0, surfaceHeightInt - 1);
final fx = x0 - x0Floor;
final fy = y0 - y0Floor;
// Get day surface colors with pre-computed indices
final idx00 = y0ClampedFloor * surfaceWidthInt + x0ClampedFloor;
final idx10 = y0ClampedFloor * surfaceWidthInt + x0Ceil;
final idx01 = y0Ceil * surfaceWidthInt + x0ClampedFloor;
final idx11 = y0Ceil * surfaceWidthInt + x0Ceil;
final c00 = surfaceData[idx00];
final c10 = surfaceData[idx10];
final c01 = surfaceData[idx01];
final c11 = surfaceData[idx11];
// Extract RGBA components for day surface
final r00 = (c00 >> 0) & 0xFF;
final g00 = (c00 >> 8) & 0xFF;
final b00 = (c00 >> 16) & 0xFF;
final a00 = (c00 >> 24) & 0xFF;
final r10 = (c10 >> 0) & 0xFF;
final g10 = (c10 >> 8) & 0xFF;
final b10 = (c10 >> 16) & 0xFF;
final a10 = (c10 >> 24) & 0xFF;
final r01 = (c01 >> 0) & 0xFF;
final g01 = (c01 >> 8) & 0xFF;
final b01 = (c01 >> 16) & 0xFF;
final a01 = (c01 >> 24) & 0xFF;
final r11 = (c11 >> 0) & 0xFF;
final g11 = (c11 >> 8) & 0xFF;
final b11 = (c11 >> 16) & 0xFF;
final a11 = (c11 >> 24) & 0xFF;
// Bilinear interpolation for day surface
var r = ((r00 * (1 - fx) + r10 * fx) * (1 - fy) +
(r01 * (1 - fx) + r11 * fx) * fy)
.round()
.clamp(0, 255);
var g = ((g00 * (1 - fx) + g10 * fx) * (1 - fy) +
(g01 * (1 - fx) + g11 * fx) * fy)
.round()
.clamp(0, 255);
var b = ((b00 * (1 - fx) + b10 * fx) * (1 - fy) +
(b01 * (1 - fx) + b11 * fx) * fy)
.round()
.clamp(0, 255);
var a = ((a00 * (1 - fx) + a10 * fx) * (1 - fy) +
(a01 * (1 - fx) + a11 * fx) * fy)
.round()
.clamp(0, 255);
// Apply day/night blending if enabled
if (hasDayNightCycle) {
final dayFactor = _calculateDayNightFactor(lat, lon);
// Get night surface colors
final nightWidth = widget.controller.nightSurface!.width.toDouble();
final nightHeight =
widget.controller.nightSurface!.height.toDouble();
final nightXRate = (nightWidth - 1) / (2.0 * math.pi);
final nightYRate = (nightHeight - 1) / math.pi;
// Invert the x coordinate to fix horizontal mirroring
final nx0 = (nightWidth - 1) - (lon + math.pi) * nightXRate;
final ny0 = (math.pi / 2 - lat) * nightYRate;
final nx0Floor = nx0.floor();
final ny0Floor = ny0.floor();
final nx0Ceil = (nx0Floor + 1).clamp(0, nightWidth.toInt() - 1);
final ny0Ceil = (ny0Floor + 1).clamp(0, nightHeight.toInt() - 1);
final nx0ClampedFloor = nx0Floor.clamp(0, nightWidth.toInt() - 1);
final ny0ClampedFloor = ny0Floor.clamp(0, nightHeight.toInt() - 1);
final nfx = nx0 - nx0Floor;
final nfy = ny0 - ny0Floor;
final nc00 = widget.controller.nightSurfaceProcessed![
(ny0ClampedFloor * nightWidth + nx0ClampedFloor).toInt()];
final nc10 = widget.controller.nightSurfaceProcessed![
(ny0ClampedFloor * nightWidth + nx0Ceil).toInt()];
final nc01 = widget.controller.nightSurfaceProcessed![
(ny0Ceil * nightWidth + nx0ClampedFloor).toInt()];
final nc11 = widget.controller.nightSurfaceProcessed![
(ny0Ceil * nightWidth + nx0Ceil).toInt()];
// Extract RGBA components for night surface
final nr00 = (nc00 >> 0) & 0xFF;
final ng00 = (nc00 >> 8) & 0xFF;
final nb00 = (nc00 >> 16) & 0xFF;
final na00 = (nc00 >> 24) & 0xFF;
final nr10 = (nc10 >> 0) & 0xFF;
final ng10 = (nc10 >> 8) & 0xFF;
final nb10 = (nc10 >> 16) & 0xFF;
final na10 = (nc10 >> 24) & 0xFF;
final nr01 = (nc01 >> 0) & 0xFF;
final ng01 = (nc01 >> 8) & 0xFF;
final nb01 = (nc01 >> 16) & 0xFF;
final na01 = (nc01 >> 24) & 0xFF;
final nr11 = (nc11 >> 0) & 0xFF;
final ng11 = (nc11 >> 8) & 0xFF;
final nb11 = (nc11 >> 16) & 0xFF;
final na11 = (nc11 >> 24) & 0xFF;
// Bilinear interpolation for night surface
final nr = ((nr00 * (1 - nfx) + nr10 * nfx) * (1 - nfy) +
(nr01 * (1 - nfx) + nr11 * nfx) * nfy)
.round()
.clamp(0, 255);
final ng = ((ng00 * (1 - nfx) + ng10 * nfx) * (1 - nfy) +
(ng01 * (1 - nfx) + ng11 * nfx) * nfy)
.round()
.clamp(0, 255);
final nb = ((nb00 * (1 - nfx) + nb10 * nfx) * (1 - nfy) +
(nb01 * (1 - nfx) + nb11 * nfx) * nfy)
.round()
.clamp(0, 255);
final na = ((na00 * (1 - nfx) + na10 * nfx) * (1 - nfy) +
(na01 * (1 - nfx) + na11 * nfx) * nfy)
.round()
.clamp(0, 255);
// Blend day and night colors based on dayFactor
r = (r * dayFactor + nr * (1 - dayFactor)).round().clamp(0, 255);
g = (g * dayFactor + ng * (1 - dayFactor)).round().clamp(0, 255);
b = (b * dayFactor + nb * (1 - dayFactor)).round().clamp(0, 255);
a = (a * dayFactor + na * (1 - dayFactor)).round().clamp(0, 255);
}
// Apply edge anti-aliasing alpha
a = (a * edgeAlpha).round().clamp(0, 255);
// Premultiply RGB by alpha for correct blending
r = (r * edgeAlpha).round().clamp(0, 255);
g = (g * edgeAlpha).round().clamp(0, 255);
b = (b * edgeAlpha).round().clamp(0, 255);
final color = (a << 24) | (b << 16) | (g << 8) | r;
spherePixels[(sphereY + x - minX).toInt()] = color;
}
}
}
final completer = Completer<SphereImage>();
ui.decodeImageFromPixels(spherePixels.buffer.asUint8List(), width.toInt(),
height.toInt(), ui.PixelFormat.rgba8888, (image) {
final sphereImage = SphereImage(
image: image,
radius: sphereRadius,
origin: Offset(-minX, -minY),
offset: Offset(maxWidth / 2, maxHeight / 2),
);
// Cache the result
_cachedSphereImage = sphereImage;
_updateCacheParams(maxWidth, maxHeight);
_isBuildingSphere = false;
completer.complete(sphereImage);
});
return completer.future;
}