a esfera dá uma volta completa, e quando vai dar mais uma volta, a rotação simplesmente inverte para o lado oposto:


import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' hide Colors;
import 'dart:math' as math;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('3D Sphere with Fixed Texture')),
        body: Center(child: SphereWidget()),
        backgroundColor: const Color.fromARGB(255, 52, 13, 245),
      ),
    );
  }
}

class SphereWidget extends StatefulWidget {
  @override
  _SphereWidgetState createState() => _SphereWidgetState();
}

class _SphereWidgetState extends State<SphereWidget> {
  double angle = 0;
  ui.Image? texture;

  @override
  void initState() {
    super.initState();
    loadImageFromAsset('assets/1.jpg');
    rotate();
  }

  void rotate() {
    Timer.periodic(Duration(milliseconds: 16), (timer) {
      setState(() {
        angle = (angle + 0.5) % 360; // Normalizing between 0 and 360
      });
    });
  }

  Future<void> loadImageFromAsset(String path) async {
    final completer = Completer<ui.Image>();
    final image = AssetImage(path);
    image.resolve(ImageConfiguration()).addListener(
      ImageStreamListener((info, _) {
        completer.complete(info.image);
      }),
    );
    texture = await completer.future;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    if (texture == null) {
      return Container();
    }
    return CustomPaint(
      size: const Size(200, 200), // Adjust the sphere size as needed
      painter: SpherePainter(texture!, angle),
    );
  }
}

class SpherePainter extends CustomPainter {
  final ui.Image texture;
  final double angle;
  final Matrix4 transform = Matrix4.identity();
  final Vector3 position = Vector3.zero();
  final Vector3 rotation = Vector3.zero();
  final Vector3 scale = Vector3(1, 1, 1);
  final double radius = 100;
  final int latSegments = 100;
  final int lonSegments = 100;

  late List<Vector3> vertices;
  late List<Offset> texcoords;
  late List<List<int>> indices;

  SpherePainter(this.texture, this.angle) {
    vertices = List<Vector3>.filled(
        (latSegments + 1) * (lonSegments + 1), Vector3.zero());
    texcoords =
        List<Offset>.filled((latSegments + 1) * (lonSegments + 1), Offset.zero);
    indices = [];

    _generateSphere(); // Initialize sphere geometry only once
  }

  void _generateSphere() {
    int i = 0;
    for (int y = 0; y <= latSegments; ++y) {
      final double v = y / latSegments;
      final double sv = math.sin(v * math.pi);
      final double cv = math.cos(v * math.pi);
      for (int x = 0; x <= lonSegments; ++x) {
        final double u = x / lonSegments;
        vertices[i] = Vector3(
          radius * math.cos(u * math.pi * 2.0) * sv, // X
          radius * cv, // Y
          radius * math.sin(u * math.pi * 2.0) * sv, // Z
        );
        texcoords[i] = Offset(u, v); // Normalized UV coordinates
        i++;
      }
    }

    // Populate triangle indices
    for (int y = 0; y < latSegments; ++y) {
      final int base1 = (lonSegments + 1) * y;
      final int base2 = (lonSegments + 1) * (y + 1);
      for (int x = 0; x < lonSegments; ++x) {
        final int current = base1 + x;
        final int next = base1 + x + 1;
        final int below = base2 + x;
        final int belowNext = base2 + x + 1;

        indices.add([current, below, next]); // Lower left triangle
        indices.add([next, below, belowNext]); // Upper right triangle
      }
    }
  }

  void updateTransform() {
    final Matrix4 m = Matrix4.compose(
        position,
        Quaternion.euler(
            radians(rotation.x), radians(rotation.y), radians(rotation.z)),
        scale);
    transform.setFrom(m);
  }

  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    rotation.setValues(0, angle, 0);
    updateTransform();

    final Float32List positions = Float32List(vertices.length * 2);
    final Float32List renderTexcoords = Float32List(texcoords.length * 2);

    // Inside the paint method
    for (int i = 0; i < vertices.length; i++) {
      final Vector4 transformedVertex = transform.transformed(
          Vector4(vertices[i].x, vertices[i].y, vertices[i].z, 1.0));
      positions[i * 2] = transformedVertex.x / transformedVertex.w; // X
      positions[i * 2 + 1] = transformedVertex.y / transformedVertex.w; // Y

      Offset t = texcoords[i];

      // Corrected texture coordinates to fit the entire texture over the sphere
      double u = t.dx * texture.width; // Map U coordinate by texture width
      double v = t.dy * texture.height; // Map V coordinate by texture height

      renderTexcoords[i * 2] = u; // U
      renderTexcoords[i * 2 + 1] = v; // V
    }

    final Uint16List indexBuffer = Uint16List(indices.length * 3);
    int indexCount = 0;
    for (var triangle in indices) {
      indexBuffer[indexCount++] = triangle[0];
      indexBuffer[indexCount++] = triangle[1];
      indexBuffer[indexCount++] = triangle[2];
    }

    final verticesData = Vertices.raw(
      VertexMode.triangles,
      positions,
      textureCoordinates: renderTexcoords,
      indices: indexBuffer,
    );

    final Float64List matrix4 = Matrix4.identity().storage;
    final paint = Paint()
      ..shader = ImageShader(
        texture,
        TileMode.clamp,
        TileMode.clamp,
        matrix4,
      );

    canvas.drawVertices(verticesData, BlendMode.srcOver, paint);
  }

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