Created
February 12, 2021 21:07
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @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