Shader Minifier is a tool that minifies and obfuscates shader code (GLSL and HLSL) without affecting its behaviour. It is also suitable for reducing the size of the shaders in other applications (e.g. webgl, games).
Its original use-case is for the demoscene, for optimizing 4k and 64k intros. In the context of 4kB intros, Shader Minifier help developers maintain and iterate on human-readable files, while shipping optimized code. Even when a shader is minified by hand by experienced demosceners, Shader Minifier is often able to optimize it further. See this 2010 report.
To be notified of new releases, use the watch feature of GitHub.
Try the online version here: https://ctrl-alt-test.fr/minifier/
- Parsing and printing of GLSL or HLSL code.
- Generation of a file (such as a C header) that can be embedded in an application.
- A command-line interface that can fit in a build.
- A web interface, for interactive workflows.
- Ability to minify multiple shaders at once, in a consistent way.
See the list of transformations below for more information. In brief:
- Strip spaces, remove comments, remove useless parens.
- Inline functions, variables and constant values.
- Simplify constant expressions:
5. * 2.
becomes10.
. - Group declarations:
float a=2.;float b;
becomesfloat a=2.,b;
. - Apply other tricks to reduce the file size.
- Simplify calls to vector constructors using vector swizzles.
- Rename variables, typically to one character.
- Remove unused local variables, unused functions and other dead code.
Other transformations try to make the code more compression friendly, e.g.
- Consistently rename vector fields (e.g. use
foo.xy
instead offoo.rg
) to help the compression. - Reuse the variable names as much as possible: a local variable may have the same name as a global variable not used in the function; two functions may have the same name using function overloading.
- Analyze the context and make statistics to compute the variable name that will be the most compression-friendly.
/* File generated with Shader Minifier 1.3
* http://www.ctrl-alt-test.fr
*/
#ifndef HEART_FRAG_EXPECTED_
# define HEART_FRAG_EXPECTED_
# define VAR_mouse "f"
# define VAR_resolution "y"
# define VAR_time "v"
const char *heart_frag =
"uniform float v;"
"uniform vec2 y;"
"uniform vec4 f;"
"void main()"
"{"
"vec2 f=(2.*gl_FragCoord.xy-y)/y.y;"
"float r=mod(v,2.)/2.,a=pow(r,.2)*.5+.5;"
"a-=a*.2*sin(r*6.2831*5.)*exp(-r*6.);"
"f*=vec2(.5,1.5)+a*vec2(.5,-.5);"
"float m=atan(f.x,f.y)/3.141593,x=length(f),e=abs(m),o=(13.*e-22.*e*e+10.*e*e*e)/(6.-5.*e),n=step(x,o)*pow(1.-x/o,.25);"
"gl_FragColor=vec4(n,0.,0.,1.);"
"}";
#endif // HEART_FRAG_EXPECTED_
Multiple output formats are available.
Download Shader Minifier here: https://github.com/laurentlb/Shader_Minifier/releases
It is a command-line tool. Without argument, it will show the usage. If you are not on Windows, you will need mono:
$ shader_minifier.exe # Windows
$ mono shader_minifier.exe # Linux, Mac...
USAGE: Shader Minifier [--help] [-o <string>] [-v] [--hlsl]
[--format <text|indented|c-variables|c-array|js|nasm|rust>]
[--field-names <rgba|xyzw|stpq>] [--preserve-externals]
[--preserve-all-globals] [--no-inlining]
[--aggressive-inlining] [--no-renaming]
[--no-renaming-list <string>] [--no-sequence]
[--smoothstep] [--no-remove-unused]
[--move-declarations] [<filename>...]
FILENAMES:
<filename>... List of files to minify
OPTIONS:
-o <string> Set the output filename (default is shader_code.h)
-v Verbose, display additional information
--hlsl Use HLSL (default is GLSL)
--format <text|indented|c-variables|c-array|js|nasm|rust>
Choose to format the output (use 'text' if you want
just the shader)
--field-names <rgba|xyzw|stpq>
Choose the field names for vectors: 'rgba', 'xyzw',
or 'stpq'
--preserve-externals Do not rename external values (e.g. uniform)
--preserve-all-globals
Do not rename functions and global variables
--no-inlining Do not automatically inline variables and functions
--aggressive-inlining Aggressively inline constants. This can reduce output
size due to better constant folding. It can also
increase output size due to repeated inlined
constants, but this increased redundancy can be
beneficial to compression, leading to a smaller final
compressed size anyway. Does nothing if inlining is
disabled.
--no-renaming Do not rename anything
--no-renaming-list <string>
Comma-separated list of functions to preserve
--no-sequence Do not use the comma operator trick
--smoothstep Use IQ's smoothstep trick
--no-remove-unused Do not remove unused code
--move-declarations Move declarations to group them
--preprocess Evaluate some of the file preprocessor directives
--help display this list of options.
In short:
-
List the shaders you want to minify on the command-line.
-
Use
-o
to choose the output file (by default, it will useshader_code.h
). If you pass-
for the output, it will be printed on stdout. -
Use
--format
to control the output format. By default, it will create a C header. There are other options to get only the shader, or have it in a Javascript, Rust, or nasm file.
4kB intros typically use a single shader file. The default flags should work well:
shader_minifier.exe -o shader_code.h fragment.glsl
We recommend that you frequently check the output of Shader Minifier to make sure there are no obvious problems. Use inlining where possible.
If you desperately need to save a few bytes, try another value of
--field-names
. It can affect the size after compression.
The recommandation is to use:
shader_minifier.exe --format c-array *.frag -o shaders.h
Then, in your C or C++ code, include the file:
const char* shaderSources[] = {
#include shaders.h
};
Note that uniforms will be renamed consistently across all the files. The
#define
lines will tell you how they were renamed. To disable this renaming,
use --preserve-externals
.
Use --format js
. It will define a variable for each shader, the variable name
being derived from the input file. We expect you to run a Javascript minifier on
the output file.
The simplest solution is to minify each file separately:
shader_minifier.exe --format text --preserve-externals file.glsl -o file_opt.glsl
The output may be used as a drop-in replacement for your original shaders.
To better understand what Shader Minifier changed and get a more readable
output, use the flags --format indented --no-renaming
. This will add some
indentation to the output, instead of using overly long lines.
Shader Minifer works by applying to the AST modifications that produce a transformed but semantically equivalent AST. The resulting assembly may be different, but the minified shader should have the same behaviour as the original one. Or at least that's the intent.
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.
Shader Minifier will preserve the preprocessor directives (the lines starting
with #
), except that it strips the spaces.
If you define a macro, Shader Minifier will notice the name of the macro and won't rename the occurrences of the macro. It also doesn't rename variables used inside the macro. Clever macros are discouraged and can break the shader.
Avoid macros that contain references to other variables, or affect how the code should be parsed. You may get a parse error in Shader Minifier, or get an output that won't compile.
With the flag --preprocess
, Shader Minifier will try to evaluate some
preprocessing directives:
#ifdef
is evaluated based on macros defined in the file.#else
is supported.- Nested
#ifdef
are supported. - Unknown directives will be preserved.
This is still experimental and there are some limitations:
- Macros declared with
#define
will be kept in the file, even if they are no longer needed. #define
and#undefine
that appear inside a#if
region are not well handled.
If you want to temporary turn off the minifier, use the //[
and //]
comments. This can be useful as a workaround if you get a parser error.
Variables inside the region won't be renamed. Spaces will be stripped.
//[
layout(local_size_x = 32) in;
//]
At this time, do not use overloaded functions (two functions with the same name but different arguments) in the input. The output probably won't compile.
On the other hand, Shader Minifier will aggressively use function overloading in the output. If two functions have a different number of arguments, they may have the same name in the output. This reduces the number of identifiers used by the shader and make it more compression friendly.
On my machine, it takes around 1s to minify a shader. A lot of that time comes from the startup time of the binary. If you have many shaders, try to minify them all at the same time by listing them all on the command-line.
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
.
We apply this technique in a few cases:
x*(y*z)
becomesy*z*x
x+(y+z)
becomesy+z+x
x+(y-z)
becomesy-z+x
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.
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.
- 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.
smoothstep(a,b,x)
calls can be replaced with
smoothstep(0.0,1.0,(x-a)/(b-a))
. When a
and b
are constant, the expression
will be simplified. In some cases, this might make the code more compressible,
and this technique was used in
Elevated.
However, in many cases this trick doesn't give good results. As a result, this
is not enabled by default; use --smoothstep
if you want to try it.
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.
-
Macros can arbitrarily change the code. Shader Minifier does not try to take into account how the future effects of macro expansion interact with minification, and assumes that the source code is valid and can be understood and minified before macro expansion. This can result in minification breaking the code.
-
Function overloading is not well supported yet, in particular when the number of arguments is not enough to disambiguate.
-
In the context of demoscene, the compressed size of a shader is more important than the uncompressed size. As a result, Shader Minifier makes some assumptions that favor minimizing the compressed size of the minified shader. In some cases, the uncompressed size of the output the can end up larger than the input.
-
Shader Minifier tries to support GLSL and HLSL. Supporting similar but different languages means that it's not always possible to take full advantage of all minification possibilities, and that the less often used corners of a language might have undetected bugs.
Please give feedback in the bugtracker. If something is blocking you, you can file a bug or update an existing bug. We rely on your feedback to prioritize the work.
Feature requests are encouraged. Add upvotes or comments to existing feature requests that are important to you.
Contributions are welcome. Please leave a message before implementing a significant change, so that we can agree on the solution.
Created by Laurent Le Brun (LLB / Ctrl-Alt-Test) and other contributors.