Как нарисовать тысячи точек на холсте в реальном времени?

Короткий вопрос: как нарисовать на холсте тысячи цветных точек в реальном времени?

Но ситуация немного сложная. Прямо сейчас структура данных выглядит так:

class Point {
  final Offset position;
  final Color color;

  Point({required this.position, required this.color});
}

... который рисуется с помощью CustomPainter в цикле for:

for (int i = firstIndex; i <= lastIndex; i++) {
  _pointPaint.color = allPointsOfChart[i].color;
  canvas.drawPoints(PointMode.points, [allPointsOfChart[i].position], _pointPaint);
}

При наборе 3000 очков на телефоне Android 12 бюджетной версии трехлетней давности (~ 120 евро) приведенный выше код удерживает растеризатор занятым на 18-22 миллисекунды. Проблема в том, что 3000 точек недостаточно, а 22 миллисекунды слишком длинны.

Кэширование операций рисования путем рисования на фоне Canvas, к сожалению, не работает из-за динамического характера данных. Кроме того, важен порядок точек, поэтому сортировать точки по цвету и раскрашивать их партиями также невозможно.

Итак... вопрос в том, возможно ли вообще нарисовать, скажем, 10 тысяч точек в реальном времени в CustomPainter? Если нет, то можно ли это реализовать с помощью специального шейдера?

Любые советы о том, как вы подойдете к этому, очень ценятся. Спасибо!

пробовали draw[Raw]Atlas уже?

pskink 18.07.2024 13:51

а если вам нужны обновления в реальном времени (при каждом кадре рендеринга и без перестройки всего дерева виджетов), проверьте, как SpritePainter использует super(repaint: ...) здесь: stackoverflow.com/questions/71332956/…

pskink 18.07.2024 19:42

@pskink Вау! Это привело к впечатляющему приросту производительности: 25 тысяч очков в среднем за 5 мс при использовании drawRawAtlas()! Большое спасибо, что поделились этим!

SePröbläm 19.07.2024 14:32

25 тысяч за 5 мс? хммм, если честно, я этого не ожидал.. ;-)

pskink 19.07.2024 15:24

Извините, это было устройство на Android 13 (все еще очень дешевое и бюджетное), которое набрало 25 тысяч баллов за 5 мс. На год старше, но тоже очень бюджетного Android 12 требуется около 15 мс для получения 25 тысяч очков. Все еще потрясающие успехи! Спасибо.

SePröbläm 19.07.2024 15:28

конечно, добро пожаловать, по сравнению с 18-22 мс для 3 тыс. очков ... все еще хорошо (~ в 10 раз быстрее) - попробуйте оптимизировать списки offsets и transform, как я написал в своем ответе, но ... это может не сильно помочь

pskink 19.07.2024 15:34

кстати, теперь я заметил, что вы использовали drawRawAtlas вместо drawAtlas - это как-то повлияло на производительность?

pskink 19.07.2024 15:39

@pskink Извините! Я забыл проследить. Ваш указатель на drawRawAtlas() был именно тем, что нужно. Гораздо эффективнее, чем начинать с собственных шейдеров... Большое спасибо за помощь!

SePröbläm 20.07.2024 16:01

@pskink Не пробовал drawAtlas(), потому что drawRawAtlas() оказался более подходящим для оптимизации производительности. Предыдущий подход drawPoints() уже был оптимизирован, пока идеи не иссякли, но все равно не сработал. В любом случае, прирост производительности довольно внушительный. Спасибо!

SePröbläm 20.07.2024 16:14

в своем ответе я скептически отнесся к тому, имеет ли drawRawAtlas большое значение, поскольку на самом деле я использовал обе и не увидел никакой разницы - но, скорее всего, так и было, поскольку я запускал обе версии на Linux для настольных компьютеров Flutter, поэтому мне было любопытно, как это выглядит на Android , возможно на мобильной платформе (андроид, ios) разница заметнее

pskink 20.07.2024 21:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

вы можете использовать Canvas.drawAtlas (или даже Canvas.drawRawAtlas, но я не думаю, что это даст вам намного лучшие результаты)

краткое объяснение:

  1. сначала нам нужен ui.Image, который требуется для метода Canvas.drawAtlas — мы могли бы получить его, декодировав какое-нибудь изображение .png или .webp, но более простой способ — использовать ui.PictureRecorder, передать его конструктору ui.Canvas и нарисовать свою «точку».
  2. rects списки содержат в каждой позиции одинаковое Rect: начиная со смещения (0, 0) и с размером, равным размеру изображения
  3. Основная идея этого решения состоит в том, чтобы использовать BlendMode.dstATop, поэтому, даже если наш ui.Image полностью черный, BlendMode.dstATop заменяет свои непрозрачные черные пиксели цветом, указанным в списке colors (это позволяет использовать любой цвет для любой «точки»)

код:

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:collection/collection.dart';

void main() => runApp(MaterialApp(home: Scaffold(body: Foo())));

class Foo extends StatefulWidget {
  @override
  State<Foo> createState() => _FooState();
}

class _FooState extends State<Foo> with TickerProviderStateMixin {
  late final controller = AnimationController(vsync: this, duration: Durations.extralong4 * 3);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () => controller.forward(from: 0),
          child: Text('animate for ${controller.duration!.inMilliseconds}ms'),
        ),
        Expanded(
          child: ClipRect(
            child: CustomPaint(
              painter: FooPainter(controller),
              child: const SizedBox.expand(),
            ),
          ),
        ),
      ],
    );
  }
}

class FooPainter extends CustomPainter {
  FooPainter(this.controller) : super(repaint: controller) {
    final recorder = ui.PictureRecorder();
    final canvas = Canvas(recorder);

    // different shapes of 'point':
    (switch (1) {
      // filled circle
      0 => const ShapeDecoration(color: Colors.black, shape: CircleBorder()),
      // outlined circle
      1 => const ShapeDecoration(shape: CircleBorder(side: BorderSide(color: Colors.black, width: R * 0.6))),
      // 8-pointed star
      _ => const ShapeDecoration(color: Colors.black, shape: StarBorder(points: 8, innerRadiusRatio: 0.6)),
    })
      .createBoxPainter(() {})
      .paint(canvas, Offset.zero, const ImageConfiguration(size: Size.fromRadius(R)));

    image = recorder.endRecording().toImageSync((2 * R).ceil(), (2 * R).ceil());
  }

  static const R = 6.0; // radius
  static const D = 3.0; // delta +/-
  static const N = 1000;

  final AnimationController controller;
  late ui.Image image;
  final rnd = Random();
  List<ui.Offset>? offsets;
  final rects = List.filled(N, Offset.zero & const Size.fromRadius(R));
  final colors = List.generate(N, (i) => HSVColor.fromAHSV(1, 120 * i / N, 1, 0.8).toColor());

  @override
  void paint(Canvas canvas, Size size) {
    offsets ??= [ // initialize offsets
      for (int i = 0; i < N; i++)
        Offset(rnd.nextDouble() * size.width, rnd.nextDouble() * size.height)
    ];
    if (controller.isAnimating) {
      offsets = [ // update offsets by random delta
        ...offsets!.mapIndexed((i, o) {
          final factor = D * ui.lerpDouble(1, 2, colors[i].green / 255)!;
          final x = (o.dx + factor * ui.lerpDouble(-1, 1, rnd.nextDouble())!).clamp(0.0, size.width);
          final y = (o.dy + factor * ui.lerpDouble(-1, 1, rnd.nextDouble())!).clamp(0.0, size.height);
          return Offset(x, y);
        })
      ];
    }
    List<ui.RSTransform> transforms = [
      ...offsets!.map((o) => ui.RSTransform(1, 0, o.dx - R, o.dy - R))
    ];
    canvas.drawAtlas(image, transforms, rects, colors, BlendMode.dstATop, null, Paint());
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

С drawRawAtlas() все отлично работает. Большое спасибо!

SePröbläm 20.07.2024 16:06

Другие вопросы по теме