Here is a list of transformations that Shader Minifier applies to the shader code.
Some of the transformations are not completely safe and may break in some corner cases. If you observe differences, don't hesitate to report a bug.
All comments are removed. Whitespace is removed whenever possible.
This transformation cannot be disabled, although you may use --format indented
to see an indented output.
Parentheses that are not needed are automatically removed. This transformations cannot be disabled; parentheses are not even recorded in the AST, they are computed based on operator precedence rules.
Input:
int a = (x * x) * (x + 1);
Output:
int a = x * x * (x + 1);
Curly braces are removed when they are not required. This happens when a block contains a single statement:
Input:
if (cond) {
return 2;
}
Output:
if (cond) return 2;
Corner case: in the example below, the curly braces around the for
are
needed because of the dangling else problem. If you remove the else
, braces
will disappear.
int dangling_else(int x)
{
if(x<0)
{
for(;;)
if(x>0)
return 0;
}
else
return 2;
}
Numbers are rewritten to take less space without loss of precision.
Additionally, constants that are approximately PI (with 8+ decimal digits) are
are replaced with a call to acos
.
Input:
0.4
0.00000012345
123456700000000
3.14159265
Output:
.4
1.2345e-7
1.234567e14
acos(-1.)
Operations with constant arguments are evaluated, e.g. 5*2
will be replaced
with 10
. This is useful especially when a value has been inlined.
if
statements, the ||
and &&
operators, as well as the ? :
ternary
operator are also simplified when a condition is statically known. This is
useful especially after inlining.
Input:
const int debug = false;
int foo() {
int a = 1;
if (debug) {
a = 2;
}
// ...
}
Output:
int foo() {
int a = 1;
// ...
}
If you write x*(y*z)
, Shader Minifier will not remove the parentheses because
it would change the orders of evaluation (this could be a problem with floating
point numbers and affect the precision of the result). Instead, it will swap
the operands and return y*z*x
. With additive operators, we assume we can
remove the parentheses.
We apply this technique in a few cases:
x*(y*z)
becomesy*z*x
x+(y+z)
becomesx+y+z
x+(y-z)
becomesx+y-z
x-(y+z)
becomesx-y-z
x-(y-z)
becomesx-y+z
When all statements in a block are expression statements (e.g. function calls or assignments) or return statements, we use the comma operators to reduce the block to one statement. This allows us to remove the curly braces.
Input:
if (x) {
y = 123;
x++;
}
Output:
if (x) y = 123,x++;
Note: This transformation doesn't always decrease the compressed file size.
Use --no-sequence
to disable it and see how it performs.
When a if always ends in a return, the else can be omitted entirely.
Input:
if(c)return a();else b();
Output:
if(c)return a();b();
When both branches of a if+else are an expression, the if is changed into a ternary operator. Additionally, if they end with an assignment to the same variable, the variable is extracted from the ternary operator.
Input:
if (c) {
x = f();
} else {
a = g();
x = 1.0;
}
Output:
x = c ? f() : a = g(), 1.0;
When both branches of a if return immediately, the if is changed into a return that uses a ternary operator.
Input:
if (y)
return z;
return w;
Output:
return y ? z : w;
while
loops are rewritten as for
loops. This sometimes enable removing semi-colons or braces,
either by moving the preceding statement into the initialization of the for
,
or by moving the last statement of the loop body into the increment part of the for
.
Input:
i = 0.;
while (i < 50) {
f(i);
i++;
}
Output:
for(i=0.;i<50;i++)f(i);
If multiple values of the same type are declared next to each other, we can merge the definitions.
Input:
float a = 1.;
float b;
float c = 2.;
Output:
float a = 1., b, c = 2.;
Note: The order of the definitions is preserved. Try to group definitions by type before running Shader Minifier.
int x;
float y;
int z; // Not optimized! Move this line above.
The next transformation will take care of this inside function blocks.
If multiple variables of the same type are declared within the same block, they will be grouped.
Input:
int a = 2;
float b = 3;
int c = 4;
Input:
int a = 2,c;
float b = 3;
c = 4;
As shown in the example, the variable name will appear twice (c
above) while
the type will be written only once (int
above). In some cases, this might make
the shader slightly longer.
Enable this transformation with the flag --move-declarations
.
We use the augmented operators (e.g. +=
) where possible.
Input:
spe=spe*spe;
x=x-.5;
a=a+(a<<3);
a=a^a>>15;
Output:
spe*=spe;
x-=.5;
a+=a<<3;
a^=a>>15;
This transformation always reduces the size of the output. However, this seems to have negligible impact on the size after compression.
To access fields of a vector, it is possible to use .rgba
fields, or .xyzw
,
or .stpq
. Shader Minifier will rename them so that it's consistent across all
the shader.
Input:
return col.r + col.g + col.b;
Outut:
return col.x + col.y + col.z;
Note: Use --field-names
to select which set of names to use. This can have
a small effect on the compressed file size.
Shader Minifier will try to automatically inline some variables. This happens when:
- the variable is used only once in the current block,
- and the variable is not used in a sub-block (e.g. inside a loop),
- and the init value is trivial (doesn't depend on a variable).
Shader Minifier will try to automatically inline some functions. This happens when:
- the function is a single return statement,
- the function is called in only one place,
- the function uses its arguments at most once,
- the function does not modify its arguments,
- the function has no out/inout arguments,
- the function does not use a global that would be shadowed by a local that is in scope at the call site.
Shader minifier will try to automatically inline an argument of a function call into the function body, as a declaration that may then be further inlined. This happens when:
- the argument value is always the same for all call sites.
- the parameter is an 'in' parameter.
- the function is not overloaded (otherwise, removing a parameter could conflict with another overload).
If inlining causes a bug in your code, you can disable it with --no-inlining
and please report a bug.
Shader Minifier can optionally/experimentally inline even more aggressively. Along with the above cases, it will inline more variables, including the variables used many times in the code.
This is enabled with --aggressive-inlining
.
Note: Inlining can lead to repetition in the shader code, which may make the shader longer, but the output may be more compression-friendly.
Shader Minifier will always inline variables and functions that start with
i_
. Inlining can allow the Minifier to simplify the code further.
For example, this input:
bool i_debug = false;
int i_level = 5;
int foo(int x) {
if (i_debug) {
x++;
}
return 2 * i_level * x;
}
will be simplified into:
int foo(int x)
{
return 10*x;
}
And this input:
float i_foo(float f, float g, float x) {
return f*x + g*x + f*g;
}
float bar(float a) {
return i_foo(2.0, 3.0, sin(sqrt(a)));
}
will be simplified into:
float bar(float a)
{
return 2.*sin(sqrt(a))+3.*sin(sqrt(a))+6.;
}
Note that the function inlining is very simplistic, only supporting functions which consist of a single return statement. (Basically, cases where you would use a macro, except this gives the minifier full visibility through it.)
If you want to aggressively reduce the size of your shader, try inlining more variables. Inlining can have performance implications though (if the variable stored the result of a computation), so be careful with it.
Shader Minifier can remove or merge assignment statements in some cases:
- if a variable is declared and reassigned on the next line;
- if a variable is reassigned on two consecutive lines;
- if a variable is reassigned and then part of a return.
Of course, this is not always possible (the variable should be read only once; side-effects might can prevent the optimization).
For example:
float d = 100.;
d = min(d, sdfBox(...));
d = min(d, sdfBall(...));
return d;
can be simplified into:
return min(min(100., sdfBox(...)), sdfBall(...));
If a local variable is no longer used and we declare a new variable of the same type, the old variable will be reused instead.
For example:
int a = 1;
// ...
f(a);
int b = 2;
// ...
f(b);
can be simplified to:
int a = 1;
// ...
f(a);
a = 2;
// ...
f(a)'
Note: This operation reduces the number of variables in the file, which usually improves the compression. When renaming is disabled, the code can be misleading, as the variable name won't match the code behavior anymore.
- Calls to
vec2
,vec3
, andvec4
can be simplified using swizzles. - Remove useless constructor, when the argument is already a vec using swizzles.
- If all arguments are equal (but not function calls), use only one argument.
- Replace floats with ints, as it is safe inside the vec constructors.
Input:
vec4(v1.x, v1.z, v2.r, v2.t);
vec2(v1.xx);
vec2(1.2, 1.2);
vec2(1.);
Output:
vec4(v1.xz,v2.xy);
v1.xx;
vec2(1.2);
vec2(1);
Function predeclarations are removed by the parser. Then Shader Minifier reorders the functions in the file, so that functions are declared before being called.
Note: Function reordering may break if there are #if
statements at the
top-level.
Local variables that are not referenced are removed.
Note: In theory, this can change the behavior of the code, if the initialization value calls a function that performs a side-effect.
The code after a return
is removed.
Note: This is optimization is disabled when there are preprocessor
directives (e.g. #if
) in the block.
Functions that are not called are removed. Functions listed with the flag
--no-renaming-list
are considered as entry-point and are not removed. By
default, this is the case for main
and mainImage
.
Note: Use --no-remove-unused
to disable this transformation.
distance(a, b)
becomeslength(a-b)
.pow(x, 1.)
becomesx
.
There are two renaming strategies:
-
If a single shader is passed on the command line, the renaming will reuse names aggressively. It will try to find the best name, based on the context where the variable is used, in order to make the code more compressible.
-
If multiple shaders are minified together, the renaming is consistent across all files. For example, all occurrences of a name (even if it's declared multiple times in different functions or files) will get renamed the same way. So if you have duplicate code in different files, that code will still compress well with this renaming strategy.
Renaming variables and functions in a compression-friendly way is difficult. Future versions of Shader Minifier may use different heuristics.