2
0

glsl_processor.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # Copyright (c) 2025-2026 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. #!/usr/bin/env python3
  18. import sys, re, zlib, struct, argparse
  19. from pathlib import Path
  20. # === Processing Passes === #
  21. def process_includes(shader_content, base_path, included_files=None):
  22. """Recursively resolve #include directives in GLSL shader code."""
  23. if included_files is None:
  24. included_files = set()
  25. base_path = Path(base_path)
  26. include_pattern = re.compile(r'^\s*#include\s+"([^"]+)"', re.MULTILINE)
  27. def replacer(match):
  28. file_path = (base_path / match.group(1)).resolve()
  29. if file_path in included_files:
  30. return ""
  31. if not file_path.is_file():
  32. print(f"Include not found: {file_path}", file=sys.stderr)
  33. return ""
  34. included_files.add(file_path)
  35. content = file_path.read_text(encoding="utf-8")
  36. return process_includes(content, file_path.parent, included_files) + "\n"
  37. return include_pattern.sub(replacer, shader_content)
  38. def remove_comments(shader_content):
  39. """Remove C-style comments"""
  40. shader_content = re.sub(r'/\*.*?\*/', '', shader_content, flags=re.DOTALL)
  41. shader_content = re.sub(r'//.*?(?=\n|$)', '', shader_content)
  42. return shader_content
  43. def remove_newlines(shader_content):
  44. """Remove unnecessary newlines while preserving preprocessor directive spacing"""
  45. lines = [line for line in shader_content.splitlines() if line.strip()]
  46. if not lines:
  47. return ""
  48. result = []
  49. for i, line in enumerate(lines):
  50. is_directive = line.lstrip().startswith('#')
  51. prev_directive = i > 0 and lines[i-1].lstrip().startswith('#')
  52. next_directive = i < len(lines)-1 and lines[i+1].lstrip().startswith('#')
  53. if i > 0 and (is_directive or prev_directive or next_directive):
  54. result.append("\n")
  55. result.append(line)
  56. return "".join(result)
  57. def normalize_spaces(shader_content):
  58. """Remove redundant spaces around operators and symbols, excluding preprocessor directives"""
  59. lines = shader_content.split('\n')
  60. processed_lines = []
  61. symbols = [
  62. ',', '.', '(', ')', '{', '}', ';', ':',
  63. '+', '-', '*', '/', '=', '<', '>',
  64. '!', '?', '|', '&'
  65. ]
  66. for line in lines:
  67. if line.lstrip().startswith('#'):
  68. line = re.sub(r'^\s*#', '#', line) # Remove spaces before the '#'
  69. line = re.sub(r'[ \t]+', ' ', line) # Replace consecutive spaces to one
  70. processed_lines.append(line)
  71. else:
  72. # Apply normalization to other lines
  73. processed_line = line
  74. for symbol in symbols:
  75. escaped = re.escape(symbol)
  76. processed_line = re.sub(rf'[ \t]+{escaped}', symbol, processed_line)
  77. processed_line = re.sub(rf'{escaped}[ \t]+', symbol, processed_line)
  78. processed_line = re.sub(r'[ \t]+', ' ', processed_line)
  79. processed_lines.append(processed_line)
  80. return '\n'.join(processed_lines)
  81. def optimize_float_literals(shader_content):
  82. """Optimize float literal notation"""
  83. shader_content = re.sub(r'\b(\d+)\.0+(?!\d)', r'\1.', shader_content) # 1.000 -> 1.
  84. shader_content = re.sub(r'\b0\.([1-9]\d*)\b', r'.\1', shader_content) # 0.5 -> .5 (but no 0.0 -> .0)
  85. return shader_content
  86. # === Main === #
  87. def process_shader(filepath):
  88. """Process a shader file through all optimization passes"""
  89. filepath = Path(filepath)
  90. try:
  91. with open(filepath, 'r', encoding='utf-8') as f:
  92. shader_content = f.read()
  93. except FileNotFoundError:
  94. print(f"Error: File not found: {filepath}", file=sys.stderr)
  95. sys.exit(1)
  96. except Exception as e:
  97. print(f"Error reading file: {e}", file=sys.stderr)
  98. sys.exit(1)
  99. shader_content = process_includes(shader_content, filepath.parent)
  100. shader_content = remove_comments(shader_content)
  101. shader_content = remove_newlines(shader_content)
  102. shader_content = normalize_spaces(shader_content)
  103. shader_content = optimize_float_literals(shader_content)
  104. return shader_content
  105. def compress_shader(shader_content):
  106. """Compresses the content with zlib (DEFLATE) then encodes in base64"""
  107. shader_bytes = shader_content.encode('utf-8')
  108. uncompressed_size = len(shader_bytes)
  109. shader_compressed = zlib.compress(shader_bytes)
  110. header = struct.pack('<Q', uncompressed_size)
  111. return header + shader_compressed
  112. def main():
  113. parser = argparse.ArgumentParser(description="Process and optionally compress a GLSL shader file.")
  114. parser.add_argument("shader_path", help="Path to the input shader file")
  115. parser.add_argument("output_file", nargs="?", help="Output file path (optional, defaults to stdout)")
  116. parser.add_argument("--compress", "-c", action="store_true", help="Compress the shader output (binary mode)")
  117. args = parser.parse_args()
  118. if args.compress and not args.output_file:
  119. sys.exit("Error: Cannot output compressed data to stdout. Please specify an output file.")
  120. formatted_shader = process_shader(args.shader_path)
  121. if args.compress:
  122. formatted_shader = compress_shader(formatted_shader)
  123. try:
  124. if args.output_file:
  125. mode = 'wb' if args.compress else 'w'
  126. with open(args.output_file, mode, encoding=None if args.compress else 'utf-8') as f:
  127. f.write(formatted_shader)
  128. else:
  129. print(formatted_shader, end="")
  130. except OSError as e:
  131. sys.exit(f"Error writing to output: {e}")
  132. if __name__ == "__main__":
  133. main()