glsl_minifier.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. # Copyright (c) 2024-2025 Le Juez Victor
  2. #
  3. # This software is provided "as-is", without any express or implied warranty. In no event
  4. # will the authors be held liable for any damages arising from the use of this software.
  5. #
  6. # Permission is granted to anyone to use this software for any purpose, including commercial
  7. # applications, and to alter it and redistribute it freely, subject to the following restrictions:
  8. #
  9. # 1. The origin of this software must not be misrepresented; you must not claim that you
  10. # wrote the original software. If you use this software in a product, an acknowledgment
  11. # in the product documentation would be appreciated but is not required.
  12. #
  13. # 2. Altered source versions must be plainly marked as such, and must not be misrepresented
  14. # as being the original software.
  15. #
  16. # 3. This notice may not be removed or altered from any source distribution.
  17. import sys, re, string, itertools
  18. glsl_keywords = {
  19. # Preprocessor directives
  20. 'core',
  21. # Data types
  22. 'void', 'bool', 'uint', 'int', 'float',
  23. 'ivec2', 'uvec2', 'vec2', 'ivec3', 'uvec3', 'vec3', 'ivec4', 'uvec4', 'vec4',
  24. 'mat2', 'mat3', 'mat4', 'struct', 'double', 'dvec2', 'dvec3', 'dvec4',
  25. 'dmat2', 'dmat3', 'dmat4',
  26. # SSBO & image qualifiers
  27. 'buffer', 'readonly', 'writeonly',
  28. # Attributes and qualifiers
  29. 'uniform', 'attribute', 'varying', 'const', 'in', 'out', 'inout', 'layout',
  30. 'binding', 'location', 'centroid', 'sample', 'pixel', 'patch', 'vertex',
  31. 'instance', 'nonuniform', 'subroutine', 'invariant', 'precise', 'shared',
  32. 'lowp', 'mediump', 'highp', 'flat', 'smooth', 'noperspective',
  33. # Layout specifiers
  34. 'std140', 'std430', 'packed', 'column_major', 'row_major',
  35. 'offset', 'align', 'set', 'push_constant', 'input_attachment_index',
  36. 'constant_id', 'local_size_x', 'local_size_y', 'local_size_z',
  37. # Programming keywords
  38. 'for', 'while', 'if', 'else', 'return', 'main',
  39. 'true', 'false', 'break', 'continue', 'discard', 'do',
  40. 'switch', 'case', 'default', # NOTE: switch case are not supported yet
  41. # Mathematical functions
  42. 'sin', 'cos', 'tan', 'min', 'max', 'mix', 'smoothstep', 'step', 'length',
  43. 'distance', 'dot', 'cross', 'normalize', 'reflect', 'refract', 'clamp',
  44. 'fract', 'ceil', 'floor', 'abs', 'sign', 'pow', 'exp', 'log', 'exp2',
  45. 'log2', 'sqrt', 'inversesqrt', 'matrixCompMult', 'transpose', 'inverse',
  46. 'determinant', 'mod', 'modf', 'isnan', 'isinf', 'ldexp',
  47. # Texture operators and functions
  48. 'texture2D', 'textureCube', 'texture2DArray', 'sampler2D', 'sampler2DShadow',
  49. 'samplerCube', 'samplerCubeShadow', 'sampler2DArray', 'samplerCubeArray',
  50. 'texture1D', 'sampler1D', 'texture1DArray', 'sampler1DArray',
  51. 'dFdx', 'dFdy', 'fwidth',
  52. # Image data types
  53. 'image2D', 'image3D', 'imageCube', 'image2DArray', 'image3DArray', 'imageCubeArray',
  54. 'image1D', 'image1DArray', 'image2DRect', 'image2DMS', 'image3DRect', 'image2DArrayMS',
  55. 'image3DArrayMS', 'image2DShadow', 'imageCubeShadow', 'image2DArrayShadow',
  56. 'imageCubeArrayShadow',
  57. # Primitives and geometry
  58. 'primitive', 'point', 'line', 'triangle', 'line_strip', 'triangle_strip', 'triangle_fan',
  59. # Global variables and coordinates
  60. 'gl_Position', 'gl_GlobalInvocationID', 'gl_LocalInvocationID', 'gl_WorkGroupID',
  61. 'gl_WorkGroupSize', 'gl_NumWorkGroups', 'gl_InvocationID', 'gl_PrimitiveID',
  62. 'gl_TessCoord', 'gl_FragCoord', 'gl_FrontFacing', 'gl_SampleID', 'gl_SamplePosition',
  63. 'gl_FragDepth', 'gl_FragStencilRef', 'gl_TexCoord', 'gl_VertexID', 'gl_InstanceID',
  64. 'gl_BaseInstance',
  65. # Tessellation and compute shaders
  66. 'tessellation', 'subpass', 'workgroup',
  67. # Atomic counters
  68. 'atomic_uint', 'atomic_int', 'atomic_float', 'atomic_counter',
  69. }
  70. def variable_renamer(input_string):
  71. """
  72. Renames all variables with short names (one letter, then two letters, etc.)
  73. while following these rules:
  74. - Do not modify variables starting with a lowercase letter followed by an uppercase letter
  75. - Do not modify names that are entirely uppercase
  76. - Do not modify definitions (#define)
  77. - Do not modify struct members
  78. - Do not modify function names
  79. - Do not modify uniform block names and their struct names
  80. """
  81. # Extract function declarations to preserve them
  82. function_pattern = r'\b(void|bool|int|float|vec\d|mat\d|[a-zA-Z_]\w*)\s+([a-zA-Z_]\w*)\s*\('
  83. function_matches = re.finditer(function_pattern, input_string)
  84. function_names = set(match.group(2) for match in function_matches)
  85. # Extract struct definitions and their members to preserve them
  86. struct_pattern = r'struct\s+(\w+)\s*\{([^}]+)\}'
  87. struct_matches = re.finditer(struct_pattern, input_string, re.DOTALL)
  88. struct_names = set()
  89. struct_members = set()
  90. for match in struct_matches:
  91. struct_names.add(match.group(1))
  92. struct_body = match.group(2)
  93. # Extract member names (after the type and before ; or ,)
  94. member_pattern = r'(?:[\w\[\]]+\s+)(\w+)(?:\s*[;,])'
  95. for member in re.finditer(member_pattern, struct_body):
  96. struct_members.add(member.group(1))
  97. # Extract uniform blocks and their instance names to preserve them
  98. uniform_block_names = set()
  99. uniform_block_members = set()
  100. # Pattern for uniform blocks: layout(...) uniform BlockName { ... } instanceName;
  101. uniform_pattern = r'layout\s*\([^)]+\)\s*uniform\s+(\w+)\s*\{([^}]+)\}\s*(\w+)\s*;'
  102. uniform_matches = re.finditer(uniform_pattern, input_string, re.DOTALL)
  103. for match in uniform_matches:
  104. block_name = match.group(1) # nom du block (ex: UniformBlock0)
  105. block_body = match.group(2) # contenu du block
  106. instance_name = match.group(3) # nom de l'instance (ex: uFrustumCurr)
  107. uniform_block_names.add(block_name)
  108. uniform_block_names.add(instance_name)
  109. # Extract member names from uniform block
  110. member_pattern = r'(?:[\w\[\]]+\s+)(\w+)(?:\s*[;,])'
  111. for member in re.finditer(member_pattern, block_body):
  112. uniform_block_members.add(member.group(1))
  113. # Alternative pattern for simple uniform blocks without layout
  114. simple_uniform_pattern = r'uniform\s+(\w+)\s*\{([^}]+)\}\s*(\w+)\s*;'
  115. simple_uniform_matches = re.finditer(simple_uniform_pattern, input_string, re.DOTALL)
  116. for match in simple_uniform_matches:
  117. block_name = match.group(1)
  118. block_body = match.group(2)
  119. instance_name = match.group(3)
  120. uniform_block_names.add(block_name)
  121. uniform_block_names.add(instance_name)
  122. # Extract member names from uniform block
  123. member_pattern = r'(?:[\w\[\]]+\s+)(\w+)(?:\s*[;,])'
  124. for member in re.finditer(member_pattern, block_body):
  125. uniform_block_members.add(member.group(1))
  126. # Retrieve all potential variable names using an improved regex (to capture variable-length identifiers)
  127. potential_vars = set(re.findall(r'(?<![\.#])\b([a-zA-Z_]\w*)\b(?!\s*\()', input_string))
  128. # Filter according to the rules
  129. variables_to_rename = []
  130. for var in potential_vars:
  131. # Exclude: variables starting with a lowercase letter followed by an uppercase letter
  132. if re.match(r'^[a-z][A-Z]', var):
  133. continue
  134. # Exclude: variables that are entirely uppercase
  135. if var.isupper():
  136. continue
  137. # Exclude struct members and struct names
  138. if var in struct_members or var in struct_names:
  139. continue
  140. # Exclude uniform block names and members
  141. if var in uniform_block_names or var in uniform_block_members:
  142. continue
  143. # Exclude function names
  144. if var in function_names:
  145. continue
  146. # Exclude GLSL keywords
  147. if var in glsl_keywords:
  148. continue
  149. # Optionally exclude macro definitions (#define) if necessary (not handled here)
  150. variables_to_rename.append(var)
  151. # Unique short name generator
  152. def name_generator():
  153. letters = string.ascii_lowercase
  154. for length in itertools.count(1):
  155. for name_tuple in itertools.product(letters, repeat=length):
  156. yield ''.join(name_tuple)
  157. gen = name_generator()
  158. new_names = {}
  159. # Sort variables to rename for deterministic order (helps debugging)
  160. for var in sorted(variables_to_rename):
  161. new_names[var] = next(gen)
  162. # Compile a regex to match ALL variables to rename, ensuring only full identifiers are replaced
  163. pattern = re.compile(
  164. r'(?<![\.#\w])(' + '|'.join(re.escape(var) for var in new_names.keys()) + r')\b(?!\s*\()'
  165. )
  166. # Replacement function for re.sub
  167. def replace_var(match):
  168. var = match.group(0)
  169. return new_names.get(var, var)
  170. modified_code = pattern.sub(replace_var, input_string)
  171. return modified_code
  172. def format_shader(input_string):
  173. """
  174. Minifies GLSL shader code by removing comments, extra whitespace, and unnecessary line breaks.
  175. Preserves preprocessor directives (#define, #version, etc.) with proper formatting.
  176. Also removes spaces after semicolons.
  177. Args:
  178. input_string (str): The GLSL shader source code as a single string.
  179. Returns:
  180. str: Minified shader code where comments are removed, code lines are compacted,
  181. and spaces arround some symbols are eliminated.
  182. """
  183. # Remove multiline comments (/* ... */) using regex with the DOTALL flag to match across multiple lines
  184. input_string = re.sub(r"/\*.*?\*/", "", input_string, flags=re.S)
  185. # Remove single-line comments (// ...) and trim whitespace from each line
  186. lines = [re.split("//", line, 1)[0].strip() for line in input_string.splitlines()]
  187. # Remove empty lines resulting from comment removal or whitespace trimming
  188. lines = [line for line in lines if line]
  189. # Rename variables before minification
  190. input_string = "\n".join(lines)
  191. input_string = variable_renamer(input_string)
  192. # Continue the minification process
  193. lines = input_string.splitlines()
  194. output = []
  195. buffer = ""
  196. for line in lines:
  197. # Preserve preprocessor directives (lines starting with #)
  198. if line.startswith("#"):
  199. # If there's accumulated code in the buffer, add it to output before processing the directive
  200. if buffer:
  201. output.append(buffer)
  202. buffer = ""
  203. output.append(line) # Preprocessor directives remain on their own lines
  204. else:
  205. # Concatenate non-directive lines into a single compact string
  206. buffer += line + " "
  207. # Add any remaining code in the buffer to the output
  208. if buffer:
  209. output.append(buffer)
  210. # Join all lines into a single string with explicit newline characters
  211. minified_code = "\\n".join(output).strip()
  212. # Remove unnecessary spaces around all specified characters
  213. minified_code = re.sub(r"\s*(;|,|\+|-|\*|/|\(|\)|{|}|\=)\s*", r"\1", minified_code)
  214. # QUICK FIX: Line correction #define to add a space before the opening parenthesis
  215. minified_code = re.sub(r'(#define\s+\w+)(\()', r'\1 \2', minified_code)
  216. return minified_code
  217. def main():
  218. """
  219. Main entry point for the script. Reads a GLSL shader file, processes it using format_shader,
  220. and outputs the minified shader code to the standard output.
  221. """
  222. if len(sys.argv) < 2:
  223. print("Usage: python glsl_minifier.py <path_to_shader>")
  224. return
  225. filepath = sys.argv[1]
  226. try:
  227. with open(filepath, "r") as file:
  228. input_shader = file.read()
  229. formatted_shader = format_shader(input_shader)
  230. print(formatted_shader, end="") # Avoids trailing newlines
  231. except FileNotFoundError:
  232. print(f"Error: File not found [{filepath}]")
  233. except Exception as e:
  234. print(f"An error occurred: {e}")
  235. if __name__ == "__main__":
  236. main()