Короткий вопрос: как нарисовать на холсте тысячи цветных точек в реальном времени?
Но ситуация немного сложная. Прямо сейчас структура данных выглядит так:
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? Если нет, то можно ли это реализовать с помощью специального шейдера?
Любые советы о том, как вы подойдете к этому, очень ценятся. Спасибо!
а если вам нужны обновления в реальном времени (при каждом кадре рендеринга и без перестройки всего дерева виджетов), проверьте, как SpritePainter
использует super(repaint: ...)
здесь: stackoverflow.com/questions/71332956/…
@pskink Вау! Это привело к впечатляющему приросту производительности: 25 тысяч очков в среднем за 5 мс при использовании drawRawAtlas()
! Большое спасибо, что поделились этим!
25 тысяч за 5 мс? хммм, если честно, я этого не ожидал.. ;-)
Извините, это было устройство на Android 13 (все еще очень дешевое и бюджетное), которое набрало 25 тысяч баллов за 5 мс. На год старше, но тоже очень бюджетного Android 12 требуется около 15 мс для получения 25 тысяч очков. Все еще потрясающие успехи! Спасибо.
конечно, добро пожаловать, по сравнению с 18-22 мс для 3 тыс. очков ... все еще хорошо (~ в 10 раз быстрее) - попробуйте оптимизировать списки offsets
и transform
, как я написал в своем ответе, но ... это может не сильно помочь
кстати, теперь я заметил, что вы использовали drawRawAtlas
вместо drawAtlas
- это как-то повлияло на производительность?
@pskink Извините! Я забыл проследить. Ваш указатель на drawRawAtlas()
был именно тем, что нужно. Гораздо эффективнее, чем начинать с собственных шейдеров... Большое спасибо за помощь!
@pskink Не пробовал drawAtlas()
, потому что drawRawAtlas()
оказался более подходящим для оптимизации производительности. Предыдущий подход drawPoints()
уже был оптимизирован, пока идеи не иссякли, но все равно не сработал. В любом случае, прирост производительности довольно внушительный. Спасибо!
в своем ответе я скептически отнесся к тому, имеет ли drawRawAtlas
большое значение, поскольку на самом деле я использовал обе и не увидел никакой разницы - но, скорее всего, так и было, поскольку я запускал обе версии на Linux для настольных компьютеров Flutter, поэтому мне было любопытно, как это выглядит на Android , возможно на мобильной платформе (андроид, ios) разница заметнее
вы можете использовать Canvas.drawAtlas
(или даже Canvas.drawRawAtlas
, но я не думаю, что это даст вам намного лучшие результаты)
краткое объяснение:
ui.Image
, который требуется для метода Canvas.drawAtlas
— мы могли бы получить его, декодировав какое-нибудь изображение .png
или .webp
, но более простой способ — использовать ui.PictureRecorder
, передать его конструктору ui.Canvas
и нарисовать свою «точку».rects
списки содержат в каждой позиции одинаковое Rect
: начиная со смещения (0, 0) и с размером, равным размеру изображения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()
все отлично работает. Большое спасибо!
пробовали
draw[Raw]Atlas
уже?