Eu não se se é pela forma que a imagem é iniciada e isso afeta a direção de rotação. Porém, acho que não, mas em ângulos de 180 e 90 graus, a bola gira corretamente, mas em ângulos da diagonal, não, por quê?

main.dart:

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:lib_sphere/sphere_lib.dart';
import 'package:vector_math/vector_math_64.dart' as vector;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: GameScreen(),
        ),
      ),
    );
  }
}

class GameScreen extends StatefulWidget {
  @override
  _GameScreenState createState() => _GameScreenState();
}

class _GameScreenState extends State<GameScreen> {
  double _latitude = 90;
  double _longitude = 0;
  Offset _positionOffset = Offset.zero;

  @override
  void initState() {
    super.initState();
    gameLoop();
  }

  void gameLoop() {
    double angle = 45; // Ângulo em graus
    double radians = vector.radians(angle); // Converte para radianos

    Timer.periodic(Duration(milliseconds: 16), (timer) {
      setState(() {
        // Incrementa a latitude e longitude com base no ângulo desejado
        _latitude +=
            cos(radians) * 1; // Ajuste o fator para controlar a velocidade
        _longitude +=
            sin(radians) * 1; // Ajuste o fator para controlar a velocidade
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Sphere(
          surface: 'assets/1.jpg',
          radius: 30,
          latitude: _longitude,
          longitude: _latitude,
          positionOffset: Offset(0, 0),
        ),
        /*
        Sphere(
          surface: 'assets/ball.jpg',
          radius: 30,
          latitude: _latitude,
          longitude: _longitude,
          positionOffset: Offset(15, 0),
        ),*/
      ],
    );
  }
}

lib da esfera:


library sphere;

import 'dart:async';
import 'dart:typed_data';
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

class Sphere extends StatefulWidget {
  const Sphere({
    Key? key,
    required this.surface,
    required this.radius,
    this.latitude = 0,
    this.longitude = 0,
    this.alignment = Alignment.center,
    this.positionOffset =
        Offset.zero, // Offset de posição para atualizar externamente
  }) : super(key: key);

  final String surface;
  final double radius;
  final double latitude;
  final double longitude;
  final Alignment alignment;
  final Offset positionOffset; // Novo parâmetro para controlar a posição

  @override
  _SphereState createState() => _SphereState();
}

class _SphereState extends State<Sphere> {
  Uint32List? surface;
  double surfaceWidth = 0;
  double surfaceHeight = 0;

  @override
  void initState() {
    super.initState();
    _loadSurface();
  }

  void _loadSurface() {
    rootBundle.load(widget.surface).then((data) {
      ui.decodeImageFromList(data.buffer.asUint8List(), (image) {
        image.toByteData(format: ui.ImageByteFormat.rawRgba).then((pixels) {
          surface = pixels!.buffer.asUint32List();
          surfaceWidth = image.width.toDouble();
          surfaceHeight = image.height.toDouble();
          setState(() {}); // Redesenhar a esfera ao carregar a textura
        });
      });
    });
  }

  Future<SphereImage?> _buildSphere(double maxWidth, double maxHeight) async {
    if (surface == null) return null;

    final double rotationX = widget.latitude * math.pi / 180;
    final double rotationZ = widget.longitude * math.pi / 180;
    final double r = widget.radius;

    final minX = math.max(-r, (-1 - widget.alignment.x) * maxWidth / 2);
    final minY = math.max(-r, (-1 + widget.alignment.y) * maxHeight / 2);
    final maxX = math.min(r, (1 - widget.alignment.x) * maxWidth / 2);
    final maxY = math.min(r, (1 + widget.alignment.y) * maxHeight / 2);
    final width = maxX - minX;
    final height = maxY - minY;
    if (width <= 0 || height <= 0) return null;
    final sphere = Uint32List(width.toInt() * height.toInt());

    var angle = math.pi / 2 - rotationX;
    final sinx = math.sin(angle);
    final cosx = math.cos(angle);
    angle = rotationZ + math.pi / 2;
    final sinz = math.sin(angle);
    final cosz = math.cos(angle);

    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 z = r * r - x * x - y * y;
        if (z > 0) {
          z = math.sqrt(z);
          var x1 = x, y1 = y, z1 = z;
          double x2, y2, z2;

          y2 = y1 * cosx - z1 * sinx;
          z2 = y1 * sinx + z1 * cosx;
          y1 = y2;
          z1 = z2;

          x2 = x1 * cosz - y1 * sinz;
          y2 = x1 * sinz + y1 * cosz;
          x1 = x2;
          y1 = y2;

          final lat = math.asin(z1 / r);
          final lon = math.atan2(y1, x1);

          final x0 = (lon + math.pi) * surfaceXRate;
          final y0 = (math.pi / 2 - lat) * surfaceYRate;

          final color = surface![(y0.toInt() * surfaceWidth + x0).toInt()];
          sphere[(sphereY + x - minX).toInt()] = color;
        }
      }
    }

    final completer = Completer<SphereImage>();
    ui.decodeImageFromPixels(
      sphere.buffer.asUint8List(),
      width.toInt(),
      height.toInt(),
      ui.PixelFormat.rgba8888,
      (image) {
        final sphereImage = SphereImage(
          image: image,
          radius: r,
          origin: Offset(-minX, -minY),
          offset: Offset(
                (widget.alignment.x + 1) * maxWidth / 2,
                (widget.alignment.y + 1) * maxHeight / 2,
              ) +
              widget.positionOffset,
        );
        completer.complete(sphereImage);
      },
    );
    return completer.future;
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return FutureBuilder(
          future: _buildSphere(constraints.maxWidth, constraints.maxHeight),
          builder: (context, AsyncSnapshot<SphereImage?> snapshot) {
            return CustomPaint(
              painter: SpherePainter(snapshot.data),
              size: Size(constraints.maxWidth, constraints.maxHeight),
            );
          },
        );
      },
    );
  }
}

class SphereImage {
  SphereImage(
      {required this.image,
      required this.radius,
      required this.origin,
      required this.offset});
  final ui.Image image;
  final double radius;
  final Offset origin;
  final Offset offset;
}

class SpherePainter extends CustomPainter {
  SpherePainter(this.sphereImage);
  final SphereImage? sphereImage;

  @override
  void paint(Canvas canvas, Size size) {
    if (sphereImage == null) return;
    final paint = Paint();
    final offsetWithPosition = sphereImage!.offset;
    final rect = Rect.fromCircle(
        center: offsetWithPosition, radius: sphereImage!.radius - 1);

    final path = Path.combine(
      PathOperation.intersect,
      Path()..addOval(rect),
      Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)),
    );

    canvas.clipPath(path);
    canvas.drawImage(
        sphereImage!.image, offsetWithPosition - sphereImage!.origin, paint);

    final gradient = RadialGradient(
      center: Alignment.center,
      colors: [
        Colors.transparent,
        Colors.black.withOpacity(0.35),
        Colors.black.withOpacity(0.5)
      ],
      stops: [0.1, 0.85, 1.0],
    );
    paint.shader = gradient.createShader(rect);
    canvas.drawRect(rect, paint);
  }

  @override
  bool shouldRepaint(covariant SpherePainter oldDelegate) {
    return true;
  }
}