buildSphere method

Future<SphereImage?> buildSphere(
  1. double maxWidth,
  2. double maxHeight
)

Implementation

Future<SphereImage?> buildSphere(double maxWidth, double maxHeight) async {
  if (widget.controller.surface == null ||
      widget.controller.surfaceProcessed == null) {
    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 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 surfaceWidth = widget.controller.surface?.width.toDouble();
  final surfaceHeight = widget.controller.surface?.height.toDouble();

  final spherePixels = Uint32List(width.toInt() * height.toInt());

  // Prepare rotation matrices
  final rotationMatrixX = Matrix3.rotationX(math.pi / 2 - rotationX);
  // final rotationMatrixY = Matrix3.rotationY(math.pi / 2 - rotationY);
  final rotationMatrixZ = Matrix3.rotationZ(rotationZ + math.pi / 2);

  final surfaceXRate = (surfaceWidth! - 1) / (2.0 * math.pi);
  final surfaceYRate = (surfaceHeight! - 1) / math.pi;

  for (var y = minY; y < maxY; y++) {
    final sphereY = (height - y + minY - 1).toInt() * width;
    for (var x = minX; x < maxX; x++) {
      var zSquared = sphereRadius * sphereRadius - x * x - y * y;
      if (zSquared > 0) {
        var z = math.sqrt(zSquared);
        var vector = Vector3(x, y, z);

        // Apply rotations
        vector = rotationMatrixX.transform(vector);
        // vector = rotationMatrixY.transform(vector);
        vector = rotationMatrixZ.transform(vector);

        final lat = math.asin(vector.z / sphereRadius);
        final lon = math.atan2(vector.y, vector.x);

        final x0 = (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, surfaceWidth.toInt() - 1);
        final y0Ceil = (y0Floor + 1).clamp(0, surfaceHeight.toInt() - 1);
        final x0ClampedFloor = x0Floor.clamp(0, surfaceWidth.toInt() - 1);
        final y0ClampedFloor = y0Floor.clamp(0, surfaceHeight.toInt() - 1);

        final fx = x0 - x0Floor;
        final fy = y0 - y0Floor;

        // Get day surface colors
        final c00 = widget.controller.surfaceProcessed![
            (y0ClampedFloor * surfaceWidth + x0ClampedFloor).toInt()];
        final c10 = widget.controller.surfaceProcessed![
            (y0ClampedFloor * surfaceWidth + x0Ceil).toInt()];
        final c01 = widget.controller.surfaceProcessed![
            (y0Ceil * surfaceWidth + x0ClampedFloor).toInt()];
        final c11 = widget.controller
            .surfaceProcessed![(y0Ceil * surfaceWidth + x0Ceil).toInt()];

        // 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;

          final nx0 = (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);
        }

        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),
    );
    completer.complete(sphereImage);
  });
  return completer.future;
}