glsl_minifier.py 9.6 KB

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