Skip to content

Instantly share code, notes, and snippets.

@yjbanov
Created February 12, 2021 21:07
Show Gist options
  • Save yjbanov/1d239dbf0aebf34f7784660b1bdc6d38 to your computer and use it in GitHub Desktop.
Save yjbanov/1d239dbf0aebf34f7784660b1bdc6d38 to your computer and use it in GitHub Desktop.
Render thousands of sprites fast by batching them into a single drawAtlas call
// @dart=2.12
import 'dart:async';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart' hide Image;
List<Sprite> sprite = <Sprite>[];
int spriteint = 10000;
final html.DivElement spriteCountIndicator = html.DivElement();
final html.ButtonElement renderMethodSwitch = html.ButtonElement()
..innerText = 'Switch renderer';
final html.DivElement renderMethodIndicator = html.DivElement();
late Picture backgroundPicture;
int? lastLoop;
Paint red = Paint()..color = const Color(0xFFFF0000);
Paint orange = Paint()..color = const Color(0xFFFF8000);
Paint yellow = Paint()..color = const Color(0xFFFFFF05);
enum RenderMethod {
drawImage,
drawVertices,
imageShader,
drawAtlas,
}
RenderMethod method = RenderMethod.drawAtlas;
main() async {
await Sprite.createSpriteFrame();
WidgetsFlutterBinding.ensureInitialized();
backgroundPicture = createBackgroundPicture();
WidgetsBinding.instance!.window.onBeginFrame = gameLoop;
WidgetsBinding.instance!.window.scheduleFrame();
generateSprites();
while (html.document.querySelectorAll('flt-glass-pane').isEmpty) {
await Future.delayed(Duration(milliseconds: 100));
}
final html.ButtonElement addSpritesButton = html.ButtonElement();
addSpritesButton.innerText = '+5000';
addSpritesButton.style
..position = 'fixed'
..top = '10px'
..right = '10px';
spriteCountIndicator.style
..position = 'fixed'
..top = '40px'
..right = '10px'
..color = 'black';
renderMethodSwitch.style
..position = 'fixed'
..top = '70px'
..right = '10px'
..color = 'black';
renderMethodIndicator.style
..position = 'fixed'
..top = '100px'
..right = '10px'
..color = 'black';
updateRenderMethodIndicator();
addSpritesButton.onClick.listen((_) {
spriteint += 5000;
generateSprites();
});
renderMethodSwitch.onClick.listen((_) {
method = RenderMethod.values[(RenderMethod.values.indexOf(method) + 1) % RenderMethod.values.length];
updateRenderMethodIndicator();
});
html.document.body!
..append(addSpritesButton)
..append(spriteCountIndicator)
..append(renderMethodSwitch)
..append(renderMethodIndicator);
}
void updateRenderMethodIndicator() {
String label;
switch (method) {
case RenderMethod.drawImage:
label = 'drawImage';
break;
case RenderMethod.drawVertices:
label = 'drawVertices';
break;
case RenderMethod.imageShader:
label = 'imageShader';
break;
case RenderMethod.drawAtlas:
label = 'drawAtlas';
break;
}
renderMethodIndicator.innerText = label;
}
void generateSprites() {
sprite = <Sprite>[];
Sprite.drawAtlasRects = Float32List(4 * spriteint);
Sprite.rstTransforms = Float32List(4 * spriteint);
for (var i = 0; i < spriteint; i++) {
sprite.add(Sprite());
final int rectOffset = 4 * i;
Sprite.drawAtlasRects[rectOffset + 2] = Sprite.spriteWidth;
Sprite.drawAtlasRects[rectOffset + 3] = Sprite.spriteHeight;
Sprite.rstTransforms[rectOffset] = 1;
Sprite.rstTransforms[rectOffset + 1] = 0;
}
spriteCountIndicator.innerText = '$spriteint sprites total';
}
Picture createBackgroundPicture() {
final PictureRecorder pictureRecorder = PictureRecorder();
final Canvas canvas = Canvas(
pictureRecorder,
Rect.fromLTWH(0.0, 0.0, WidgetsBinding.instance!.window.physicalSize.width,
WidgetsBinding.instance!.window.physicalSize.height));
canvas.drawPaint(red);
canvas.drawCircle(Offset(200, 300), 150, yellow);
canvas.drawCircle(Offset(500, 500), 250, orange);
return pictureRecorder.endRecording();
}
void gameLoop(Duration d) {
final loop = d.inMicroseconds;
final loopDelta = loop - (lastLoop ?? loop);
lastLoop = loop;
for (var i = 0; i < spriteint; i++) {
sprite[i].update(loopDelta);
}
final SceneBuilder builder = SceneBuilder();
builder.addPicture(Offset.zero, backgroundPicture);
final PictureRecorder pictureRecorder = PictureRecorder();
final Canvas canvas = Canvas(
pictureRecorder,
Rect.largest,
);
switch (method) {
case RenderMethod.drawImage:
for (var i = 0; i < spriteint; i++) {
sprite[i].renderUsingDrawImage(canvas);
}
break;
case RenderMethod.drawVertices:
for (var i = 0; i < spriteint; i++) {
sprite[i].renderUsingDrawVertices(canvas);
}
break;
case RenderMethod.imageShader:
for (var i = 0; i < spriteint; i++) {
sprite[i].renderUsingImageShader(canvas);
}
break;
case RenderMethod.drawAtlas:
for (var i = 0; i < spriteint; i++) {
sprite[i].renderUsingDrawAtlas(canvas, i);
}
canvas.drawRawAtlas(
Sprite.sprite,
Sprite.rstTransforms,
Sprite.drawAtlasRects,
null,
null,
null,
Sprite.defaultPaint,
);
break;
}
builder.addPicture(Offset.zero, pictureRecorder.endRecording());
WidgetsBinding.instance!.window.render(builder.build());
WidgetsBinding.instance!.window.scheduleFrame();
}
final math.Random random = math.Random();
class Sprite {
static late Image sprite;
static late Vertices vertices;
static final Paint paintWithImageShader = Paint()..isAntiAlias = false;
static final Paint defaultPaint = Paint()..isAntiAlias = false;
static double spriteWidth = 0;
static double spriteHeight = 0;
static late Rect rect;
static late Float32List drawAtlasRects;
static late Float32List rstTransforms;
double x = random.nextDouble() * 1000;
double y = random.nextDouble() * 1000;
double dx = -8 + random.nextDouble() * 16;
double dy = -4 + random.nextDouble() * 8;
void update(int loopDelta) {
x += dx * loopDelta / 16000;
y += dy * loopDelta / 16000;
if ((x < 0 && dx < 0) || (x > 1000 && dx > 0)) dx = -dx;
if ((y < 0 && dy < 0) || (y > 1000 && dy > 0)) dy = -dy;
}
void renderUsingDrawVertices(Canvas canvas) {
canvas
..save()
..translate(x, y)
..drawVertices(vertices, BlendMode.srcOver, paintWithImageShader)
..restore();
}
void renderUsingImageShader(Canvas canvas) {
canvas
..save()
..translate(x, y)
..drawRect(rect, paintWithImageShader)
..restore();
}
void renderUsingDrawImage(Canvas canvas) {
canvas.drawImage(sprite, Offset(x, y), defaultPaint);
}
void renderUsingDrawAtlas(Canvas canvas, int spriteIndex) {
rstTransforms[4 * spriteIndex + 2] = x;
rstTransforms[4 * spriteIndex + 3] = y;
}
static Future<void> createSpriteFrame() {
Uint8List lst = Uint8List.fromList([
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 60,
0, 0, 0, 72, 8, 6, 0, 0, 0, 170, 79, 150, 165, 0, 0, 0, 4, 103, 65, 77,
65, 0, 0, 177, 142, 124, 251, 81, 147, 0, 0, 2, 94, 73, 68, 65, 84, 120,
156, 237, 155, 61, 78, 195, 64, 16, 133, 199, 40, 161, 160, 74, 132, 92,
80, 82, 70, 66, 17, 13, 13, 23, 160, 132, 130, 43, 32, 174, 225, 35, 208,
162, 92, 129, 130, 156, 131, 46, 68, 226, 8, 41, 172, 40, 169, 168, 34,
133, 110, 61, 94, 237, 120, 102, 227, 117, 136, 244, 252, 42, 139, 108,
118, 231, 105, 62, 15, 251, 151, 140, 152, 246, 197, 158, 52, 101, 69,
166, 182, 33, 34, 189, 163, 116, 10, 6, 36, 121, 57, 235, 52, 148, 19, 20,
156, 225, 26, 14, 18, 6, 13, 24, 171, 232, 254, 188, 92, 170, 65, 76, 102,
107, 181, 13, 239, 167, 161, 189, 138, 55, 92, 134, 225, 12, 15, 164, 15,
98, 49, 182, 160, 203, 37, 97, 249, 187, 88, 184, 231, 139, 233, 52, 216,
94, 106, 227, 197, 22, 52, 0, 151, 97, 56, 195, 34, 210, 158, 146, 99,
204, 177, 148, 20, 219, 198, 195, 59, 40, 184, 12, 195, 25, 174, 33, 109,
153, 39, 167, 170, 198, 22, 73, 136, 198, 98, 204, 5, 151, 97, 56, 195,
77, 85, 58, 249, 18, 207, 82, 117, 185, 222, 63, 164, 16, 190, 131, 109,
94, 159, 245, 87, 18, 46, 195, 112, 134, 155, 24, 112, 172, 196, 86, 102,
46, 203, 156, 57, 149, 188, 138, 237, 188, 245, 203, 67, 36, 137, 85, 250,
225, 250, 220, 33, 49, 153, 173, 15, 198, 219, 184, 83, 17, 37, 142, 110,
236, 171, 1, 151, 97, 56, 195, 53, 164, 121, 53, 227, 243, 234, 187, 171,
65, 18, 188, 83, 169, 77, 133, 135, 203, 48, 156, 97, 211, 142, 199, 215,
106, 151, 100, 48, 254, 10, 88, 150, 117, 93, 76, 78, 224, 50, 12, 103,
216, 116, 20, 72, 29, 159, 6, 74, 203, 64, 203, 114, 175, 65, 253, 92,
154, 8, 208, 176, 105, 121, 200, 37, 239, 66, 28, 79, 30, 234, 42, 247,
61, 210, 72, 242, 39, 30, 73, 48, 182, 84, 215, 46, 94, 13, 203, 29, 21,
184, 12, 195, 25, 246, 217, 115, 76, 164, 194, 216, 114, 44, 210, 114,
172, 254, 218, 82, 147, 224, 12, 91, 15, 196, 131, 178, 96, 44, 253, 157,
227, 205, 251, 57, 160, 122, 171, 247, 58, 184, 224, 50, 12, 103, 88, 68,
154, 99, 86, 20, 71, 137, 133, 136, 136, 86, 75, 125, 210, 194, 227, 137,
141, 13, 46, 195, 112, 134, 125, 164, 57, 79, 106, 185, 148, 208, 146,
142, 66, 164, 137, 199, 49, 95, 25, 184, 12, 195, 25, 54, 77, 60, 218, 84,
197, 83, 192, 152, 11, 46, 195, 112, 134, 77, 27, 191, 121, 94, 61, 151,
101, 85, 189, 255, 11, 75, 62, 110, 158, 87, 30, 202, 82, 255, 46, 92,
134, 225, 12, 155, 14, 196, 57, 198, 167, 38, 75, 108, 247, 195, 161, 51,
3, 151, 97, 56, 195, 173, 118, 60, 110, 223, 70, 169, 226, 112, 122, 218,
110, 213, 54, 159, 163, 240, 184, 143, 155, 141, 123, 158, 143, 199, 188,
79, 135, 61, 92, 134, 225, 12, 71, 35, 45, 225, 20, 43, 9, 191, 54, 99,
73, 253, 112, 193, 101, 24, 206, 176, 248, 115, 90, 239, 23, 46, 193, 127,
238, 169, 240, 110, 83, 153, 99, 251, 132, 203, 48, 156, 97, 107, 149, 14,
110, 238, 89, 80, 60, 64, 73, 198, 186, 25, 84, 23, 98, 151, 187, 234, 38,
33, 92, 134, 225, 12, 255, 1, 102, 172, 210, 13, 246, 211, 202, 107, 0, 0,
0, 0, 73, 69, 78, 68, 174, 66, 96, 130]);
final completer = Completer<void>();
instantiateImageCodec(lst).then((codec) {
codec.getNextFrame().then((frameInfo) {
sprite = frameInfo.image;
spriteWidth = sprite.width.toDouble();
spriteHeight = sprite.height.toDouble();
rect = Rect.fromLTRB(0, 0, spriteWidth, spriteHeight);
paintWithImageShader.shader = ImageShader(sprite, TileMode.repeated, TileMode.repeated, Matrix4.identity().storage);
vertices = Vertices(
VertexMode.triangleFan,
[Offset.zero, Offset(spriteWidth, 0), Offset(spriteWidth, spriteHeight), Offset(0, spriteHeight)],
textureCoordinates: [Offset.zero, Offset(spriteWidth, 0), Offset(spriteWidth, spriteHeight), Offset(0, spriteHeight)],
);
completer.complete();
});
});
return completer.future;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment