Android’s new custom pixel shader AGSL in Compose
In this next part of the series, we will dive into the process of writing pixel shaders. We will explore the syntax, functions, and techniques required to create custom shaders that can produce stunning and dynamic visual effects. Whether you’re new to shader programming or looking to expand your knowledge, this article will provide practical insights and examples to help you write your pixel shaders effectively.
If you want to learn how to use shaders in Jetpack Compose, please first read the following article:
Using Android’s New Custom Pixel Shader AGSL in Compose: Part 1
Drawing complex shapes and intricate patterns in a shader requires a different approach than using canvas draw calls like drawLine, drawCircle, or drawRect. AGSL lacks support for primitive drawing commands, so you must implement everything from scratch. So, if you aim to draw simple lines and shapes, it’s often easier and more convenient to utilize canvas draw calls.
However, if your objective is to achieve smooth transitions, intricate gradients, or intricate visual effects, using a shader can offer significant advantages. Shaders excel at creating complex and dynamic visual transformations beyond what basic draw calls can accomplish.
When drawing shapes in AGSL, you need a distance function that describes the shape. This function allows you to calculate the distance from the current pixel’s screen coordinate to the shape’s outline. For instance, when drawing a circle, the distance function can be defined as the vector length between the pixel’s coordinate and the center point’s coordinate. AGSL provides a handy function length(vec2) that can calculate this length for you. By comparing the calculated distance to the circle’s radius, you can easily differentiate between pixels inside and outside the circle.
half4 main(vec2 fragCoord) {
// Window size is 200x200
vec2 circlePos = vec2(100, 100); // Center
float radius = 100.0;
float d = length(fragCoord - circlePos) - radius;
float inside = 0.0;
if (d < 0.0) inside = 1.0;
return vec4(vec3(1.0-inside),1);
}
Hard transitions between shapes can often result in aliasing effects, where jagged or pixelated edges occur. To mitigate these issues and achieve smoother transitions, a function called smoothstep can help. This function allows you to soften the edges of your shape by providing a gradual transition between two specified points.
When using smoothstep(a, b, f), you provide a starting point a and an ending point b that defines the range of the transition. The smoothstep function then maps the original shape’s values to a smooth range between 0 and 1 at the transition start and endpoints.
float a = 20.0;
float d = length(fragCoord - circlePos) - (radius-a);
float inside = smoothstep(0.0, a, d);
You can find distance functions for different shapes and apply them similarly to the circle. Inigo Quilez’s articles are a valuable resource for simple shape distance functions: https://iquilezles.org/articles/distfunctions2d/
Using simple shapes alone seems boring initially, but they can be transformed into an extraordinary background with the right approach.
For example, one of our designers at Seven Principles created an impressive background using only triangles. Inspired by their design, I’ve recreated this background using shader code, utilizing the smoothstep function and rotation/transformations.
The simple shapes can also be combined into more complex ones.
The Union of two shapes can be archived by combining both distance functions using float d = min(d1,d2). The intersection is just a max(d1,d2), and it is also possible to subtract one shape from the other using max(d1,-d2).
If we want to deform shapes, the key lies in manipulating the coordinate system rather than altering the distance function. Do not try to bend the spoon. That’s impossible. Instead, try to bend the universe.
Using a simple 2D rotation matrix on the pixel coordinates before calculating the distance:
mat2 rotate(float a) {
return mat2(cos(a), sin(a), -sin(a), cos(a));
}
This function will rotate around the origin of the coordinate system (0,0). In AGSL, the origin is in the top left corner. When we want to rotate around the center of our shape, it is important to shift the coordinate system before rotation to the center and, after rotation, back to its original origin. For example, when the center is at 100,100, we need to subtract vec2(100,100), apply the rotation, and add vec2(100,100). The following sample demonstrates how to rotate a box:
// distance function of a box
// from Inigo Quilez
// p - position
// b - size vec2(width,height)
float sdBox(vec2 p, vec2 b) {
vec2 d = abs(p)-b;
return length(max(d,0.0)) + min(max(d.x,d.y),0.0);
}
half4 main(vec2 fragCoord) {
vec2 centerPos = vec2(100,100);
fragCoord -= centerPos; // Move 0,0 to center
float angleDegree = 45;
float angleRad = angleDegree / 360. * PI * 2.0;
fragCoord *= rot(angleRad); // rotate 45 degree
fragCoord += centerPos; // Move 0,0 to left, top
float d = sdBox(fragCoord - centerPos, vec2(100, 30));
return vec4(vec3(d),1);
}
When manipulating the coordinate system, it’s important to remember that the process often involves thinking in reverse. For example, if you intend to stretch a shape horizontally, you must shrink the coordinate system along that direction. By multiplying the x-axis by 2, the shape will be compressed by half, appearing squashed.
half4 main(vec2 fragCoord) {
vec2 centerPos = vec2(100,100);
fragCoord -= centerPos;
fragCoord *= vec2(2,1); // Multiply x-axe
float d = length(fragCoord) - 100.0;
return vec4(vec3(d),1);
}
Combining the knowledge gained from this article series, spanning from part 1 to part 3, we can achieve stunning backgrounds within our app. As demonstrated earlier, I created captivating visual effects using simple triangle shapes. Additionally, I employed uniforms to dynamically modify the shader’s colors based on the device’s dark and light modes.
Custom shaders offer immense potential for crafting distinctive visual effects within your Android app. While this article series has hopefully provided valuable insights into AGSL, many aspects are worth exploring. For example, we could delve into topics like shader animation, limitations, backward compatibility, or even AGSL usage in Compose for desktop applications.
To ensure that future articles match your interests, please let me know which areas you want to explore. Share your preferences, and together, we’ll continue to learn about the world of custom shaders in Android app development.
Learn How to Create Outstanding AGSL Shaders for Android was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.