Tutorial: Creating a Chrome Ball with PixiEditor 2
I learnt about PixiEditor at the Libre Graphics Meeting in Nuremberg. It looks like the main focus is on creating pixel art for games. However, version 2.0 features a very powerful node graph tool. To compare it with GIMP, I tried to reproduce the Chrome Ball tutorial using PixiEditor.
It took me two evenings to learn the necessary skills, as there are significant differences.
Firstly, I don't need multiple layers; I can do everything I need to do with one layer and nodes. Even the gradient is a node.
The biggest problem was the 'Panorama Projection'. PixiEditor has no filter or function like this. However, it does have a Skia Shader node where you can write your own shader in SKSL, a variant of GLSL.
Initially, I tried to map the image around a sphere, which worked after a lot of learning and testing. However, the result was nice but not what I was looking for. Just before giving up, I had the idea of looking into the code of the GIMP/GEGL 'Panorama Projection' filter. I was able to transfer the logic to my Skia shader.
First of all you need a equirectangular panorama. If you don't have one you can get one from HDRI Haven. For this tutorial I took this one: https://hdrihaven.com/hdri/?h=bell_park_pier
Open the image and switch directly to "Graph View". Save the file as .pixi straight away, as only this file format can save all the graphs.
Let's take a look at the different notes I used. My first goal was to create three 'layers'.
- The first layer is the background, unchanged. This will become the background.
- The second layer is the background flipped vertically and covering ~20% of the image height. This will become the chrome ball.
- The third layer is a black-to-transparent gradient, slightly higher than the second layer, which will be used as a shadow.
2. The second 'layer' was more complicated. First, I used the 'Scale' node to reduce its size. Then I added the 'Compose Matrix' node and set the 'Scale Y' parameter to -1 to flip it. I then used the 'Separate Vector' node to get the height of the image and move it to the bottom.
3. For the gradient, I used 'Create Image' to get an empty, transparent layer. As this should be the same size as the original image, I took the size output from the base image and plugged it into the size input from the 'Create Image' node. Then I used the 'Gradient' node to fill the image.
Now, I needed to composite these three 'layers' together. To achieve this, I used the 'Merge' node twice.
5. Now comes the hard part: The 'Shader' node. This is where you can insert your own code. You will find my code below. Finally, you need to connect the shader's output to the 'Output' node.
The shader code very heavyly inpired by the GEGL panorama-projection.c:
* panorama-projection shader
*
* Copyright (C) 2026 Tobias Jakobs
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
uniform shader iImage; // default Parameter
uniform float pan; // horizontal rotation (deg)
uniform float tilt; // vertical rotation (deg)
uniform float zoom; // field of view (like GEGL zoom)
// degrees to radians
float rad(float d) {
return d * PI / 180.0;
}
// camera rotation (pan/tilt/spin)
float3 rotateCamera(float3 dir)
{
float yaw = rad(pan);
float pitch = rad(tilt);
// Y axis (pan)
float cy = cos(yaw);
float sy = sin(yaw);
dir = float3(cy*dir.x + sy*dir.z, dir.y, -sy*dir.x + cy*dir.z);
// X axis (tilt)
float cx = cos(pitch);
float sx = sin(pitch);
dir = float3(dir.x, cx*dir.y - sx*dir.z, sx*dir.y + cx*dir.z);
return dir;
}
// direction to equirectangular UV
float2 dirToEquirect(float3 dir)
{
float u = atan(dir.z, dir.x) / (2.0 * PI) + 0.5;
float v = acos(clamp(dir.y, -1.0, 1.0)) / PI;
return float2(u, v);
}
half4 main(float2 fragCoord)
{
float2 uv = (fragCoord * 2.0 - iResolution.xy) / iResolution.y;
// FOV like GEGL zoom (smaller zoom = wider view)
float fov = radians(zoom);
float3 ray = normalize(float3(uv * tan(fov), -1.0));
// apply camera rotation (pan/tilt)
ray = rotateCamera(ray);
// sample panorama
float2 texUV = dirToEquirect(ray);
float3 color = iImage.eval(texUV * iResolution).rgb;
return half4(color, 1.0);
}
Overall, I must say it was an interesting journey. PixiEditor can do a lot; the node engine is very powerful, especially with the shaders. I would recommend it to anyone who needs to create pixel art or other graphics for games. However, it still has a long way to go before it can be considered a general photo editor. The main downside is that it freezes for a few seconds from time to time. I think I'll use PixiEditor in future as a complement to GIMP, especially if I need filters or effects that GIMP doesn't have built in. From my experience, it's much faster to write a new shader than a new GIMP plugin.
Kommentare
Kommentar veröffentlichen