glsl_minifier.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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', 'vec2', 'ivec3', 'vec3', 'ivec4', 'vec4',
  24. 'mat2', 'mat3', 'mat4', 'struct', 'double', 'dvec2', 'dvec3', 'dvec4',
  25. 'dmat2', 'dmat3', 'dmat4',
  26. # Attributes and uniforms
  27. 'uniform', 'attribute', 'varying', 'const', 'in', 'out', 'inout', 'layout',
  28. 'binding', 'location', 'centroid', 'sample', 'pixel', 'patch', 'vertex',
  29. 'instance', 'nonuniform', 'subroutine', 'invariant', 'precise', 'shared',
  30. 'lowp', 'mediump', 'highp', 'flat', 'smooth', 'noperspective',
  31. # Programming keywords
  32. 'for', 'while', 'if', 'else', 'return', 'main',
  33. 'true', 'false', 'break', 'continue', 'discard', 'do',
  34. # Mathematical functions
  35. 'sin', 'cos', 'tan', 'min', 'max', 'mix', 'smoothstep', 'step', 'length',
  36. 'distance', 'dot', 'cross', 'normalize', 'reflect', 'refract', 'clamp',
  37. 'fract', 'ceil', 'floor', 'abs', 'sign', 'pow', 'exp', 'log', 'exp2',
  38. 'log2', 'sqrt', 'inversesqrt', 'matrixCompMult', 'transpose', 'inverse',
  39. 'determinant', 'mod', 'modf', 'isnan', 'isinf', 'ldexp',
  40. # Texture operators and functions
  41. 'texture2D', 'textureCube', 'texture2DArray', 'sampler2D', 'sampler2DShadow',
  42. 'samplerCube', 'samplerCubeShadow', 'sampler2DArray', 'samplerCubeArray',
  43. 'texture1D', 'sampler1D', 'texture1DArray', 'sampler1DArray',
  44. 'dFdx', 'dFdy', 'fwidth',
  45. # Image data types
  46. 'image2D', 'image3D', 'imageCube', 'image2DArray', 'image3DArray', 'imageCubeArray',
  47. 'image1D', 'image1DArray', 'image2DRect', 'image2DMS', 'image3DRect', 'image2DArrayMS',
  48. 'image3DArrayMS', 'image2DShadow', 'imageCubeShadow', 'image2DArrayShadow',
  49. 'imageCubeArrayShadow',
  50. # Primitives and geometry
  51. 'primitive', 'point', 'line', 'triangle', 'line_strip', 'triangle_strip', 'triangle_fan',
  52. # Global variables and coordinates
  53. 'gl_Position', 'gl_GlobalInvocationID', 'gl_LocalInvocationID', 'gl_WorkGroupID',
  54. 'gl_WorkGroupSize', 'gl_NumWorkGroups', 'gl_InvocationID', 'gl_PrimitiveID',
  55. 'gl_TessCoord', 'gl_FragCoord', 'gl_FrontFacing', 'gl_SampleID', 'gl_SamplePosition',
  56. 'gl_FragDepth', 'gl_FragStencilRef', 'gl_TexCoord', 'gl_VertexID', 'gl_InstanceID',
  57. # Tessellation and compute shaders
  58. 'tessellation', 'subpass', 'workgroup',
  59. # Atomic counters
  60. 'atomic_uint', 'atomic_int', 'atomic_float', 'atomic_counter',
  61. }
  62. def variable_renamer(input_string):
  63. """
  64. Renames all variables with short names (one letter, then two letters, etc.)
  65. while following these rules:
  66. - Do not modify variables starting with a lowercase letter followed by an uppercase letter
  67. - Do not modify names that are entirely uppercase
  68. - Do not modify definitions (#define)
  69. - Do not modify struct members
  70. - Do not modify function names
  71. """
  72. # Extract function declarations to preserve them
  73. function_pattern = r'\b(void|bool|int|float|vec\d|mat\d|[a-zA-Z_]\w*)\s+([a-zA-Z_]\w*)\s*\('
  74. function_matches = re.finditer(function_pattern, input_string)
  75. function_names = set(match.group(2) for match in function_matches)
  76. # Extract struct definitions and their members to preserve them
  77. struct_pattern = r'struct\s+(\w+)\s*\{([^}]+)\}'
  78. struct_matches = re.finditer(struct_pattern, input_string, re.DOTALL)
  79. struct_names = set()
  80. struct_members = set()
  81. for match in struct_matches:
  82. struct_names.add(match.group(1))
  83. struct_body = match.group(2)
  84. # Extract member names (after the type and before ; or ,)
  85. member_pattern = r'(?:[\w\[\]]+\s+)(\w+)(?:\s*[;,])'
  86. for member in re.finditer(member_pattern, struct_body):
  87. struct_members.add(member.group(1))
  88. # Retrieve all potential variable names using an improved regex (to capture variable-length identifiers)
  89. potential_vars = set(re.findall(r'(?<![\.#])\b([a-zA-Z_]\w*)\b(?!\s*\()', input_string))
  90. # Filter according to the rules
  91. variables_to_rename = []
  92. for var in potential_vars:
  93. # Exclude: variables starting with a lowercase letter followed by an uppercase letter
  94. if re.match(r'^[a-z][A-Z]', var):
  95. continue
  96. # Exclude: variables that are entirely uppercase
  97. if var.isupper():
  98. continue
  99. # Exclude struct members and struct names
  100. if var in struct_members or var in struct_names:
  101. continue
  102. # Exclude function names
  103. if var in function_names:
  104. continue
  105. # Exclude GLSL keywords
  106. if var in glsl_keywords:
  107. continue
  108. # Optionally exclude macro definitions (#define) if necessary (not handled here)
  109. variables_to_rename.append(var)
  110. # Unique short name generator
  111. def name_generator():
  112. letters = string.ascii_lowercase
  113. for length in itertools.count(1):
  114. for name_tuple in itertools.product(letters, repeat=length):
  115. yield ''.join(name_tuple)
  116. gen = name_generator()
  117. new_names = {}
  118. # Sort variables to rename for deterministic order (helps debugging)
  119. for var in sorted(variables_to_rename):
  120. new_names[var] = next(gen)
  121. # Compile a regex to match ALL variables to rename, ensuring only full identifiers are replaced
  122. pattern = re.compile(
  123. r'(?<![\.#\w])(' + '|'.join(re.escape(var) for var in new_names.keys()) + r')\b(?!\s*\()'
  124. )
  125. # Replacement function for re.sub
  126. def replace_var(match):
  127. var = match.group(0)
  128. return new_names.get(var, var)
  129. modified_code = pattern.sub(replace_var, input_string)
  130. return modified_code
  131. def format_shader(input_string):
  132. """
  133. Minifies GLSL shader code by removing comments, extra whitespace, and unnecessary line breaks.
  134. Preserves preprocessor directives (#define, #version, etc.) with proper formatting.
  135. Also removes spaces after semicolons.
  136. Args:
  137. input_string (str): The GLSL shader source code as a single string.
  138. Returns:
  139. str: Minified shader code where comments are removed, code lines are compacted,
  140. and spaces arround some symbols are eliminated.
  141. """
  142. # Remove multiline comments (/* ... */) using regex with the DOTALL flag to match across multiple lines
  143. input_string = re.sub(r"/\*.*?\*/", "", input_string, flags=re.S)
  144. # Remove single-line comments (// ...) and trim whitespace from each line
  145. lines = [re.split("//", line, 1)[0].strip() for line in input_string.splitlines()]
  146. # Remove empty lines resulting from comment removal or whitespace trimming
  147. lines = [line for line in lines if line]
  148. # Rename variables before minification
  149. input_string = "\n".join(lines)
  150. input_string = variable_renamer(input_string)
  151. # Continue the minification process
  152. lines = input_string.splitlines()
  153. output = []
  154. buffer = ""
  155. for line in lines:
  156. # Preserve preprocessor directives (lines starting with #)
  157. if line.startswith("#"):
  158. # If there's accumulated code in the buffer, add it to output before processing the directive
  159. if buffer:
  160. output.append(buffer)
  161. buffer = ""
  162. output.append(line) # Preprocessor directives remain on their own lines
  163. else:
  164. # Concatenate non-directive lines into a single compact string
  165. buffer += line + " "
  166. # Add any remaining code in the buffer to the output
  167. if buffer:
  168. output.append(buffer)
  169. # Join all lines into a single string with explicit newline characters
  170. minified_code = "\\n".join(output).strip()
  171. # Remove unnecessary spaces around all specified characters
  172. minified_code = re.sub(r"\s*(;|,|\+|-|\*|/|\(|\)|{|}|\=)\s*", r"\1", minified_code)
  173. # QUICK FIX: Line correction #define to add a space before the opening parenthesis
  174. minified_code = re.sub(r'(#define\s+\w+)(\()', r'\1 \2', minified_code)
  175. return minified_code
  176. def main():
  177. """
  178. Main entry point for the script. Reads a GLSL shader file, processes it using format_shader,
  179. and outputs the minified shader code to the standard output.
  180. """
  181. if len(sys.argv) < 2:
  182. print("Usage: python glsl_minifier.py <path_to_shader>")
  183. return
  184. filepath = sys.argv[1]
  185. try:
  186. with open(filepath, "r") as file:
  187. input_shader = file.read()
  188. formatted_shader = format_shader(input_shader)
  189. print(formatted_shader, end="") # Avoids trailing newlines
  190. except FileNotFoundError:
  191. print(f"Error: File not found [{filepath}]")
  192. except Exception as e:
  193. print(f"An error occurred: {e}")
  194. if __name__ == "__main__":
  195. main()