Skip to content

Commit c54c15f

Browse files
kornellapuspydon
andauthored
docs: Added basic shader tutorial (#3799)
Added Basic Shader Tutirial to Flame documentation. Added one note to CONTRIBUTING.md, which explains how to resolve non UTF-8 characters in path. --------- Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com> Co-authored-by: Lukas Klingsbo <me@lukas.fyi>
1 parent bc81e7f commit c54c15f

12 files changed

Lines changed: 534 additions & 4 deletions

File tree

.github/.cspell/gamedev_dictionary.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jank # stutter or inconsistent gap or timing
2727
lerp # short for linear interpolation
2828
LTRBR # left top right bottom radius
2929
LTWH # left top width height
30+
mediump # medium GLSL float precision
3031
metalness # a measure of how much a surface reflects light for the purposes of physically based rendering
3132
Minkowski # Minkowski sum, a sum of two sets of vectors, A and B, where the result is the sum of each vector pair
3233
multitap # support from a device to recognize many taps at the same time
@@ -49,6 +50,7 @@ subfolders # plural of subfolders
4950
sublists # plural of sublist
5051
subrange # a range entirely contained on a given range
5152
SVGs # plural of SVG
53+
texel # texture pixel (unit of texture map)
5254
texels # plural of texel
5355
tileset # image with a collection of tiles. in games, tiles are small square sprites laid out in a grid to form the game map
5456
tilesets # plural of tileset

.github/.cspell/people_usernames.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ erickzanardo # github.com/erickzanardo
77
feroult # github.com/feroult
88
fröber # github.com/Brixto
99
gnarhard # github.com/gnarhard
10+
Hoodead # github.com/kornellapu
1011
kenney # kenney.nl
1112
Klingsbo # github.com/spydon
13+
Kornél # github.com/kornellapu
14+
lapu # Artist of reference art (basic shader tutorial)
15+
Lapu # github.com/kornellapu
1216
luan # github.com/luanpotter
1317
luanpotter # github.com/luanpotter
1418
Lukas # github.com/spydon
45.4 KB
Loading
306 KB
Loading
16 KB
Loading
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Basic shader tutorial
2+
3+
This tutorial will give you a brief understanding of how to create and use basic shaders on
4+
`SpriteComponent`s with `PostProcess` and `PostProcessComponent` using Dart/Flutter and the Flame
5+
engine.
6+
7+
This tutorial assumes that you have a working Flame project set up. If you don't, please follow
8+
the [](bare_flame_game.md) tutorial first.
9+
10+
The tutorial consists of 4 steps. We will create a simple outline shader for sprites which have a
11+
transparent background layer.
12+
13+
```{note}
14+
This tutorial is intended to work on images with transparent
15+
background, like `.png` files.
16+
```
17+
18+
*Created by Kornél (Hoodead) Lapu.*
19+
20+
21+
```{toctree}
22+
:hidden:
23+
24+
1. Sprite Component <step1.md>
25+
2. Outline Post Process <step2.md>
26+
3. Shader <step3.md>
27+
4. User Input <step4.md>
28+
5. Takeaways <takeaways.md>
29+
```
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# 1. Sprite Component
2+
3+
4+
## Architecture and Responsibilities
5+
6+
Let's create the component where we render our sprite and apply the shader. We will split this
7+
into two classes:
8+
9+
- a `SpriteComponent` subclass that loads the image and handles input events
10+
- a `PostProcessComponent` subclass that wraps the sprite and applies the shader
11+
12+
This separation means that shader changes only require editing the wrapper class, while sprite
13+
changes like adding input event mixins or additional children only require editing the sprite
14+
class.
15+
16+
17+
## Image resource
18+
19+
For this tutorial we need an image with a transparent background to apply the outline shader to.
20+
Create an `assets/images/` directory in your project and add your `.png` image there.
21+
22+
Don't forget to register the assets folder in `pubspec.yaml`:
23+
24+
```yaml
25+
flutter:
26+
assets:
27+
- assets/images/
28+
```
29+
30+
31+
## Sprite
32+
33+
Create a new file named `sword_component.dart` (replace "sword" with your own image name):
34+
35+
```dart
36+
import 'package:flame/components.dart';
37+
38+
class SwordSprite extends SpriteComponent {
39+
@override
40+
Future<void> onLoad() async {
41+
sprite = await Sprite.load('sword.png');
42+
size = sprite!.srcSize;
43+
}
44+
}
45+
```
46+
47+
48+
## Wrapper
49+
50+
Next, add the wrapper class that applies the post process. In the same file, create:
51+
52+
```dart
53+
import 'package:flame/components.dart';
54+
import 'package:flame/post_process.dart';
55+
56+
import 'package:basic_shader_tutorial/outline_postprocess.dart';
57+
58+
class OutlinedSwordSprite extends PostProcessComponent {
59+
OutlinedSwordSprite({super.position, super.anchor})
60+
: super(
61+
children: [SwordSprite()],
62+
postProcess: OutlinePostProcess(anchor: anchor ?? Anchor.topLeft),
63+
);
64+
}
65+
```
66+
67+
68+
## Result
69+
70+
The final `sword_component.dart` file looks like this:
71+
72+
```dart
73+
import 'package:flame/components.dart';
74+
import 'package:flame/post_process.dart';
75+
76+
import 'package:basic_shader_tutorial/outline_postprocess.dart';
77+
78+
class OutlinedSwordSprite extends PostProcessComponent {
79+
OutlinedSwordSprite({super.position, super.anchor})
80+
: super(
81+
children: [SwordSprite()],
82+
postProcess: OutlinePostProcess(anchor: anchor ?? Anchor.topLeft),
83+
);
84+
}
85+
86+
class SwordSprite extends SpriteComponent {
87+
@override
88+
Future<void> onLoad() async {
89+
sprite = await Sprite.load('sword.png');
90+
size = sprite!.srcSize;
91+
}
92+
}
93+
```
94+
95+
This won't compile yet because `OutlinePostProcess` doesn't exist. Let's create it in the next
96+
step!
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# 2. Outline Post Process
2+
3+
4+
## Responsibility
5+
6+
The `PostProcess` class manages the fragment (pixel) shader. It is responsible for loading the
7+
shader program, creating GPU resources, and keeping uniform variables up to date each frame. You
8+
can also expose runtime settings through uniforms, for example to enable or disable effects.
9+
10+
11+
## Post process
12+
13+
Create a new file named `outline_postprocess.dart`. This class loads the shader program in
14+
`onLoad()` and passes uniform values to the GPU each frame in `postProcess()`:
15+
16+
```dart
17+
import 'dart:ui';
18+
19+
import 'package:flutter/material.dart';
20+
21+
import 'package:flame/components.dart';
22+
import 'package:flame/post_process.dart';
23+
24+
extension on Color {
25+
Vector4 toVector4() {
26+
return Vector4(r, g, b, a);
27+
}
28+
}
29+
30+
class OutlinePostProcess extends PostProcess {
31+
final double outlineSize;
32+
Color outlineColor;
33+
final Anchor anchor;
34+
35+
OutlinePostProcess({
36+
this.outlineSize = 7.0,
37+
this.outlineColor = Colors.purpleAccent,
38+
this.anchor = Anchor.topLeft,
39+
});
40+
41+
late final FragmentProgram _fragmentProgram;
42+
late final FragmentShader _fragmentShader =
43+
_fragmentProgram.fragmentShader();
44+
late final Paint _myPaint = Paint()..shader = _fragmentShader;
45+
46+
@override
47+
Future<void> onLoad() async {
48+
await super.onLoad();
49+
50+
_fragmentProgram =
51+
await FragmentProgram.fromAsset('assets/shaders/outline.frag');
52+
}
53+
54+
@override
55+
void postProcess(Vector2 size, Canvas canvas) {
56+
final preRenderedSubtree = rasterizeSubtree();
57+
58+
_fragmentShader.setFloatUniforms((value) {
59+
value
60+
..setVector(size)
61+
..setFloat(outlineSize)
62+
..setVector(outlineColor.toVector4());
63+
});
64+
65+
_fragmentShader.setImageSampler(0, preRenderedSubtree);
66+
67+
canvas
68+
..save()
69+
..translate(-size.x * anchor.x, -size.y * anchor.y)
70+
..drawRect(Offset.zero & size.toSize(), _myPaint)
71+
..restore();
72+
}
73+
}
74+
```
75+
76+
With this file in place, the syntax error from the previous step will go away.
77+
78+
Since the `PostProcessComponent` is the parent of the `SpriteComponent`, the post process renders
79+
first and the sprite is drawn on top. The `rasterizeSubtree()` call captures all children into an
80+
image that the shader can sample from.
81+
82+
83+
## Usage
84+
85+
Now we need to wire everything together. Open `main.dart` and add both a plain sprite and an
86+
outlined sprite to the world so we can compare them side by side:
87+
88+
```dart
89+
import 'package:flutter/material.dart';
90+
91+
import 'package:flame/components.dart';
92+
import 'package:flame/game.dart';
93+
94+
import 'package:basic_shader_tutorial/sword_component.dart';
95+
96+
void main() {
97+
runApp(
98+
GameWidget(game: MyGame()),
99+
);
100+
}
101+
102+
class MyGame extends FlameGame {
103+
MyGame() : super(world: MyWorld());
104+
105+
@override
106+
Color backgroundColor() => Colors.green;
107+
}
108+
109+
class MyWorld extends World {
110+
@override
111+
Future<void> onLoad() async {
112+
add(
113+
SwordSprite()
114+
..position = Vector2(-200, 0)
115+
..anchor = Anchor.center,
116+
);
117+
118+
add(
119+
OutlinedSwordSprite(
120+
position: Vector2(200, 0),
121+
anchor: Anchor.center,
122+
),
123+
);
124+
}
125+
}
126+
```
127+
128+
Here we use a custom `FlameGame` subclass to override the background color. Adjust the positions
129+
and color to suit your own images.
130+
131+
Run the application. You should see only one sprite, the outlined one is missing. The console
132+
will show why:
133+
`[...] Unhandled Exception: Exception: Asset 'assets/shaders/outline.frag' not found [...]`
134+
135+
We haven't created the shader file yet. Let's do that in the next step.

0 commit comments

Comments
 (0)