por que em alguns momentos a troca de ângulo é abrupta da esfera?

import 'dart:async';
import 'dart:math';
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 Ball {
  Vector3 position;
  Vector3 velocity;
  double friction;
  double angle;
  ui.Image texture;

  Ball({
    required this.position,
    required this.velocity,
    required this.texture,
    this.friction = 0.98,
    this.angle = 0.0,
  });

  // Atualiza a posição e a velocidade da bola
  void update() {
    position += velocity;
    velocity *= friction;
    angle -= velocity.length * 0.05; // Ângulo para rotação contínua da esfera

    // Verifica colisão com as bordas horizontais
    if (position.y > 200 || position.y < 0) {
      // Verifica se está fora dos limites superiores e inferiores
      if (position.y > 200) {
        position.y = 200; // Reposiciona a bola na borda
      } else {
        position.y = 0; // Reposiciona a bola na borda
      }
      // Inverte a velocidade no eixo y para quicar verticalmente
      velocity = Vector3(velocity.x, -velocity.y, 0);
    }

    if (position.x > 300 || position.x < 0) {
      // Verifica se está fora dos limites laterais
      if (position.x > 300) {
        position.x = 300; // Reposiciona a bola na borda
      } else {
        position.x = 0; // Reposiciona a bola na borda
      }
      // Inverte a velocidade no eixo x para quicar horizontalmente
      velocity = Vector3(-velocity.x, velocity.y, 0);
    }
  }

  CustomPaint buildPaint() {
    return CustomPaint(
      size: const Size(100, 100),
      painter: SpherePainter(
          texture, angle, position, atan2(velocity.y, velocity.x)),
    );
  }
}

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 ui.Color.fromARGB(255, 110, 19, 19),
      ),
    );
  }
}

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

class _SphereWidgetState extends State<SphereWidget> {
  double angle = 0;
  ui.Image? texture;
  late Ball ball;
  bool started = false;

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

  void initializeBall() {
    ball = Ball(
        position: Vector3(200, 10, 0),
        velocity: Vector3.zero(),
        texture: texture!,
        angle: angle);
  }

  void rotate() {
    Timer.periodic(Duration(milliseconds: 16), (timer) {
      setState(() {
        angle += 0.01; // Incrementa 0.5 graus em radianos

        if (started) ball.update();
      });
    });
  }

  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;
    // Agora que a textura foi carregada, podemos inicializar o `ball`
    initializeBall();
    started = true;

    setState(() {});
  }

  void moveBall(double angle, double force) {
    ball.velocity.x += force * math.cos(radians(angle));
    ball.velocity.y += force * math.sin(radians(angle));
  }

  @override
  Widget build(BuildContext context) {
    if (texture == null) {
      return Container();
    }

    return Stack(
      children: [
        Positioned(
          left: 50,
          top: 10,
          child: ElevatedButton(
              onPressed: () {
                moveBall(Random().nextDouble() * 360, 10);
              },
              child: Text('Aqui')),
        ),
        Positioned(
            left: ball.position.x,
            top: ball.position.y,
            width: 100,
            height: 100,
            child: ball.buildPaint()),

        /*
        CustomPaint(
            size: const Size(
                100, 100), // Tamanho ajustado para melhor visualização
            painter: SpherePainter(texture!, angle))*/

        /*
        for (int i = 0; i < 10; i++)
          Transform.translate(
            offset:
                Offset(0, i * 100.0), // Espaçamento vertical entre as esferas
            child: CustomPaint(
              size: const Size(
                  100, 100), // Tamanho ajustado para melhor visualização
              painter: SpherePainter(texture!, angle),
            ),
          ),*/
      ],
    );
  }
}

class SpherePainter extends CustomPainter {
  final ui.Image texture;
  final double angle;
  Vector3 position = Vector3.zero();
  final Vector3 scale = Vector3(1, 1, 1);
  Vector3 rotation = Vector3.zero();
  double rotateAngle = 0;
  Quaternion rotationQuat = Quaternion.identity();

  // Geometria da esfera
  static List<Vector3> staticVertices = [];
  static List<Offset> staticTexcoords = [];
  static List<List<int>> staticIndices = [];
  static bool isSphereGenerated = false;

  final Matrix4 transform = Matrix4.identity();
  final double radius = 15;
  final int latSegments = 12; // Ajustado para melhor performance
  final int lonSegments = 12;

  SpherePainter(this.texture, this.angle, this.position, this.rotateAngle) {
    if (!isSphereGenerated) {
      _generateSphere();
      isSphereGenerated = true;
    }
  }

  void _generateSphere() {
    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;
        staticVertices.add(Vector3(
          radius * math.cos(u * math.pi * 2.0) * sv, // X
          radius * cv, // Y
          radius * math.sin(u * math.pi * 2.0) * sv, // Z
        ));
        staticTexcoords.add(Offset(u, v)); // Coordenadas UV normalizadas
      }
    }

    // Preenche os índices dos triângulos com ordem anti-horária consistente
    for (int y = 0; y < latSegments; ++y) {
      for (int x = 0; x < lonSegments; ++x) {
        final int current = (lonSegments + 1) * y + x;
        final int next = current + 1;
        final int below = (lonSegments + 1) * (y + 1) + x;
        final int belowNext = below + 1;

        staticIndices
            .add([current, below, next]); // Triângulo inferior esquerdo
        staticIndices
            .add([next, below, belowNext]); // Triângulo superior direito
      }
    }
  }

  void rotate(Vector3 axis, double angleRadians) {
    // Atualiza o quaternion de rotação, mas não aplica a transformação ainda
    Quaternion increment = Quaternion.axisAngle(axis, angleRadians);
    rotationQuat = increment * rotationQuat;
  }

  void updateTransform() {
    // Aplica a transformação baseada na rotação (rotationQuat), posição e escala
    final Matrix4 m = Matrix4.compose(position, rotationQuat, scale);
    transform.setFrom(m);
  }

/*
  void updateTransform() {
    // Resetar a matriz de transformação
    transform.setIdentity();

    // Aplicar a rotação contínua em torno dos eixos X e Y
    //transform.rotateX(angle); // Ângulo variável para rotação contínua em X
    transform.rotateY(angle); // Ângulo variável para rotação contínua em Y
  }*/

  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    rotate(Vector3(math.sin(-rotateAngle), math.cos(rotateAngle), 0), angle);

    updateTransform();

    final Float32List positions = Float32List(staticVertices.length * 2);
    final Float32List renderTexcoords = Float32List(staticTexcoords.length * 2);

    // Transformar os vértices
    List<Vector4> transformedVertices =
        List<Vector4>.filled(staticVertices.length, Vector4.zero());
    for (int i = 0; i < staticVertices.length; i++) {
      transformedVertices[i] = transform.transformed(Vector4(
          staticVertices[i].x, staticVertices[i].y, staticVertices[i].z, 1.0));
      // Projeção em perspectiva
      double perspective = 10000.0;
      double w = perspective / (perspective + transformedVertices[i].z);
      positions[i * 2] = transformedVertices[i].x * w; // X
      positions[i * 2 + 1] = transformedVertices[i].y * w; // Y

      Offset t = staticTexcoords[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
    }

    // Lista para armazenar triângulos visíveis
    List<Triangle> visibleTriangles = [];

    // Calcular e filtrar triângulos com base nas normais (Back-Face Culling)
    for (int i = 0; i < staticIndices.length; i++) {
      final int idx0 = staticIndices[i][0];
      final int idx1 = staticIndices[i][1];
      final int idx2 = staticIndices[i][2];

      Vector4 v0 = transformedVertices[idx0];
      Vector4 v1 = transformedVertices[idx1];
      Vector4 v2 = transformedVertices[idx2];

      // Vetores das arestas do triângulo
      Vector3 edge1 = Vector3(v1.x - v0.x, v1.y - v0.y, v1.z - v0.z);
      Vector3 edge2 = Vector3(v2.x - v0.x, v2.y - v0.y, v2.z - v0.z);

      // Normal do triângulo
      Vector3 normal = edge1.cross(edge2).normalized();

      // Vetor de visão assumindo câmera no eixo Z positivo
      Vector3 viewVector = Vector3(0, 0, 1);

      // Produto escalar entre normal e vetor de visão
      double dotProduct = normal.dot(viewVector);

      // Se o dot product for positivo, o triângulo está voltado para a câmera
      if (dotProduct > 0) {
        // Calcular profundidade média para ordenação
        double depth = (v0.z + v1.z + v2.z) / 3.0;
        visibleTriangles.add(Triangle([idx0, idx1, idx2], depth));
      }
    }

    // Ordenar triângulos do mais distante para o mais próximo
    visibleTriangles.sort((a, b) => b.depth.compareTo(a.depth));

    // Preparar buffers para Vertices.raw
    List<int> renderedIndices = [];
    List<double> renderedPositions = [];
    List<double> renderedTexcoordsList = [];

    // Mapeamento de vértices únicos para evitar duplicações
    Map<int, int> vertexMap = {};
    int currentIndex = 0;

    for (var tri in visibleTriangles) {
      for (int idx in tri.indices) {
        if (!vertexMap.containsKey(idx)) {
          vertexMap[idx] = currentIndex++;
          renderedPositions.add(positions[idx * 2]);
          renderedPositions.add(positions[idx * 2 + 1]);
          renderedTexcoordsList.add(renderTexcoords[idx * 2]);
          renderedTexcoordsList.add(renderTexcoords[idx * 2 + 1]);
        }
        renderedIndices.add(vertexMap[idx]!);
      }
    }

    if (renderedIndices.isEmpty) {
      return; // Nenhum triângulo visível para desenhar
    }

    final Uint16List indexBuffer = Uint16List.fromList(renderedIndices);
    final Float32List positionsBuffer = Float32List.fromList(renderedPositions);
    final Float32List texcoordsBuffer =
        Float32List.fromList(renderedTexcoordsList);

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

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

    // Desenhar os triângulos visíveis ordenados por profundidade
    canvas.drawVertices(verticesData, BlendMode.srcOver, paint);
  }

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

class Triangle {
  final List<int> indices;
  final double depth;

  Triangle(this.indices, this.depth);
}