blob: c1f0d0e6e9f674ed6a2c47a63e9d0fbaf84968f6 [file]
import argparse
import glob
import io
import os
import re
import sys
from collections import defaultdict
parser = argparse.ArgumentParser(description="""
Process a batch of .glsl files. Minify and export them to C++ strings.
Performs the following transformations:
* Strip comments.
* Strip whitespace.
* Strip unused #defines.
* Rename stpq and rgba swizzles to xyzw.
* Rename variables.
- No new name begins with the '_' character, so internal code can begin names with '_'
without fear of renaming collisions.
- GLSL keywords and builtins are not renamed.
- Tokens beginning with '@' have their new name exported to a header file.
- Tokens beginning with '$' are not renamed, with the exception of removing the leading '$'.
"file.glsl" gets exported to:
* "outdir/file.exports.h", with #defines for the rewritten names of each identifier that began
with '@' in the original shader.
* "outdir/file.glsl.cpp", with a global `const char file_glsl[]` in the rive::glsl
namespace that contains the minified shader code. This variable is intentionally declared as a
global in order to generate a link error if the user includes the string more than once in
the build process.
* "outdir/file.minified.glsl" for offline compiling, with all variables renamed except for
exported #defines names (since the offline compiling process will set those defines).
""")
parser.add_argument("files", type=str, nargs="+", help="a list of glsl files")
parser.add_argument("-o", "--outdir", required=True,
help="OUTPUT directory to store the header files")
parser.add_argument("-H", "--human-readable", action='store_true',
help="don't rename or strip out comments or whitespace")
parser.add_argument("-p", "--ply-path", type=str, help="path to ply module")
parser.add_argument("-m", "--msvc", action="store_true", help="set this to generate MSVC-compatible headers")
args = parser.parse_args()
if args.ply_path:
# --ply-path was specified, so add it to the sys path so we can locate the module.
# (if it was not specified we'll assume that it's already reachable via the path)
# Convert posix path to windows
convertedPath = args.ply_path
if sys.platform.startswith('win32') and args.ply_path[:2] == '/c':
convertedPath = 'C:\\' + args.ply_path[2:]
print('Using ply path:' + convertedPath)
sys.path.append(convertedPath)
import ply.lex as lex
# tokens used by PLY to run lexical analysis on our glsl files.
tokens = [
"DEFINE",
"IFDEF",
"DEFINED_ID",
"TOKEN_PASTE",
"DIRECTIVE",
"LINE_COMMENT",
"BLOCK_COMMENT",
"WHITESPACE",
"OP",
"FLOAT",
"HEX",
"INT",
"ID",
"UNKNOWN",
]
# tracks which exported identifiers (identifiers whose @name begins with '@') are used as switches
# inside an #ifdef, #if defined(), etc.
exported_switches = set()
# counts the number of times each ID is seen, to prioritize which IDs get the shortest names
all_id_counts = defaultdict(int);
all_id_reference_counts = defaultdict(int);
def parse_id(name, exports, *, is_reference):
all_id_counts[name] += 1
if is_reference:
all_id_reference_counts[name] += 1
# identifiers that begin with '@' get exported to C++ through #defines.
if name[0] == '@':
exports.add(name)
# lexing functions used by PLY.
def t_DEFINE(tok):
r"\#[ \t]*define[ \t]+(?P<id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)(?P<arglist>\((\n|[^\)])*\))?(?P<val>(((\\\n|.)(?!\/[\/\*]))*))?"
tok.define_id = re.match(t_DEFINE.__doc__, tok.value)['id']
arglist = re.match(t_DEFINE.__doc__, tok.value)['arglist']
tok.define_arglist = Minifier(arglist, "", tok.lexer.exports) if arglist else None
val = re.match(t_DEFINE.__doc__, tok.value)['val']
tok.define_val = Minifier(val, "", tok.lexer.exports) if val else None
parse_id(tok.define_id, tok.lexer.exports, is_reference=False)
tok.lexer.lineno += tok.value.count('\n')
return tok
def t_IFDEF(tok):
r"\#[ \t]*(?P<tag>ifn?def)[ \t]+(?P<ifdef_id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)"
tok.ifdef_tag = re.match(t_IFDEF.__doc__, tok.value)['tag']
tok.ifdef_id = re.match(t_IFDEF.__doc__, tok.value)['ifdef_id']
if tok.ifdef_id[0] == '@':
exported_switches.add(tok.ifdef_id)
parse_id(tok.ifdef_id, tok.lexer.exports, is_reference=True)
return tok
def t_DEFINED_ID(tok):
r"defined\((?P<defined_id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)\)"
tok.defined_id = re.match(t_DEFINED_ID.__doc__, tok.value)['defined_id']
if tok.defined_id[0] == '@':
exported_switches.add(tok.defined_id)
parse_id(tok.defined_id, tok.lexer.exports, is_reference=True)
return tok
def t_TOKEN_PASTE(tok):
r"\#\#"
return tok
def t_DIRECTIVE(tok):
r"\#[ \t]*(?P<val>(((\\\n|.)(?!\/[\/\*]))*))"
val = re.match(t_DIRECTIVE.__doc__, tok.value)['val']
tok.directive_val = Minifier(val, "", tok.lexer.exports) if val else None
tok.lexer.lineno += tok.value.count('\n')
return tok
def t_LINE_COMMENT(tok):
r"//(\\\n|.)*"
tok.lexer.lineno += tok.value.count('\n')
return tok
def t_BLOCK_COMMENT(tok):
r"\/\*(\*(?!\/)|[^*])*\*\/"
tok.lexer.lineno += tok.value.count('\n')
return tok
def t_WHITESPACE(tok):
r"(\s|\\\r?\n)+"
tok.lexer.lineno += tok.value.count('\n')
return tok
def t_OP(tok):
r"[~!%^&\*\(\)\-=+\/\[\]{}\?:\<\>\.\,|;]"
return tok
def t_FLOAT(tok):
r"([0-9]*\.[0-9]+|[0-9]+\.)([eE][+\-]?[0-9]+)?|([0-9]+[eE][+\-]?[0-9]+)"
return tok
def t_HEX(tok):
r"0x[0-9a-fA-F]+u?"
return tok
def t_INT(tok):
r"[0-9]+u?"
return tok
def t_ID(tok):
r"[\@\$]?[A-Za-z_][A-Za-z0-9_]*"
parse_id(tok.value, tok.lexer.exports, is_reference=True)
return tok
def t_UNKNOWN(tok):
r"."
return tok
def t_error(tok):
raise Exception("Illegal character '%s' at line %d" % (tok.value[0], tok.lexer.lineno))
# identifier names that cannot be renamed
glsl_reserved = {
'EmitStreamVertex', 'EmitVertex', 'EndPrimitive', 'EndStreamPrimitive',
'__VERSION__', '__pixel_localEXT', '__pixel_local_inEXT',
'__pixel_local_outEXT', 'abs', 'acos', 'acosh', 'all', 'allInvocations',
'allInvocationsEqual', 'any', 'anyInvocation', 'asin', 'asinh', 'atan',
'atanh', 'atomicAdd', 'atomicAnd', 'atomicCompSwap', 'atomicCounter',
'atomicCounterAdd', 'atomicCounterAnd', 'atomicCounterCompSwap',
'atomicCounterDecrement', 'atomicCounterExchange', 'atomicCounterIncrement',
'atomicCounterMax', 'atomicCounterMin', 'atomicCounterOr',
'atomicCounterSubtract', 'atomicCounterXor', 'atomicExchange', 'atomicMax',
'atomicMin', 'atomicOr', 'atomicXor', 'barrier',
'beginFragmentShaderOrderingINTEL', 'beginInvocationInterlockARB',
'beginInvocationInterlockNV', 'binding', 'bitCount', 'bitfieldExtract',
'bitfieldInsert', 'bitfieldReverse', 'blend_support_all_equations',
'blend_support_colorburn', 'blend_support_colordodge',
'blend_support_darken', 'blend_support_difference',
'blend_support_exclusion', 'blend_support_hardlight',
'blend_support_lighten', 'blend_support_multiply', 'blend_support_overlay',
'blend_support_screen', 'blend_support_softlight', 'bool', 'break',
'buffer', 'bvec2', 'bvec3', 'bvec4', 'case', 'ceil', 'centroid', 'clamp',
'coherent', 'const', 'constant_id', 'continue', 'cos', 'cosh', 'cross',
'dFdx', 'dFdxCoarse', 'dFdxFine', 'dFdy', 'dFdyCoarse', 'dFdyFine',
'default', 'defined', 'degrees', 'determinant', 'discard', 'distance', 'do',
'dot', 'elif', 'else', 'enable', 'endInvocationInterlockARB',
'endInvocationInterlockNV', 'endif', 'equal', 'exp', 'exp2', 'extension',
'faceforward', 'false', 'findLSB', 'findMSB', 'flat', 'float',
'floatBitsToInt', 'floatBitsToUint', 'floor', 'fma', 'for', 'fract',
'frexp', 'ftransform', 'fwidth', 'fwidthCoarse', 'fwidthFine',
'greaterThan', 'greaterThanEqual', 'groupMemoryBarrier', 'highp', 'if',
'iimage2D', 'image2D', 'imageAtomicAdd', 'imageAtomicAnd',
'imageAtomicCompSwap', 'imageAtomicExchange', 'imageAtomicMax',
'imageAtomicMin', 'imageAtomicOr', 'imageAtomicXor', 'imageLoad',
'imageSamples', 'imageSize', 'imageStore', 'imulExtended', 'in', 'inout',
'input_attachment_index', 'int', 'intBitsToFloat', 'interpolateAtCentroid',
'interpolateAtOffset', 'interpolateAtSample', 'invariant', 'inverse',
'inversesqrt', 'isampler2D', 'isampler2DArray', 'isampler3D',
'isamplerCube', 'isinf', 'isnan', 'ivec2', 'ivec3', 'ivec4', 'layout',
'ldexp', 'length', 'lessThan', 'lessThanEqual', 'location', 'log', 'log2',
'lowp', 'main', 'mat2', 'mat2x2', 'mat2x3', 'mat2x4', 'mat3', 'mat3x2',
'mat3x3', 'mat3x4', 'mat4', 'mat4x2', 'mat4x3', 'mat4x4', 'matrixCompMult',
'max', 'mediump', 'memoryBarrier', 'memoryBarrierAtomicCounter',
'memoryBarrierBuffer', 'memoryBarrierImage', 'memoryBarrierShared', 'min',
'mix', 'mod', 'modf', 'noise1', 'noise2', 'noise3', 'noise4',
'noperspective', 'normalize', 'not', 'notEqual', 'out', 'outerProduct',
'packDouble2x32', 'packHalf2x16', 'packSnorm2x16', 'packSnorm4x8',
'packUnorm2x16', 'packUnorm4x8', 'pixelLocalLoadANGLE',
'pixelLocalStoreANGLE', 'pow', 'pragma', 'precision', 'push_constant',
'r16f', 'r32f', 'r32i', 'r32ui', 'radians', 'readonly', 'reflect',
'refract', 'require', 'return', 'rg16f', 'rgb10_a2', 'rgb_2_yuv', 'rgba8',
'rgba8i', 'rgba8ui', 'round', 'roundEven', 'sampler', 'sampler2D',
'sampler2DArray', 'sampler2DArrayShadow', 'sampler2DShadow', 'sampler3D',
'samplerCube', 'samplerCubeShadow', 'set', 'shadow1D', 'shadow1DLod',
'shadow1DProj', 'shadow1DProjLod', 'shadow2D', 'shadow2DEXT', 'shadow2DLod',
'shadow2DProj', 'shadow2DProjEXT', 'shadow2DProjLod', 'sign', 'sin', 'sinh',
'smooth', 'smoothstep', 'sqrt', 'std140', 'std430', 'step', 'struct',
'subpassInput', 'subpassInputMS', 'subpassLoad', 'switch', 'tan', 'tanh',
'texelFetch', 'texelFetchOffset', 'texture', 'texture1D', 'texture1DLod',
'texture1DProj', 'texture1DProjLod', 'texture2D', 'texture2DGradEXT',
'texture2DLod', 'texture2DLodEXT', 'texture2DProj', 'texture2DProjGradEXT',
'texture2DProjLod', 'texture2DProjLodEXT', 'texture2DRect',
'texture2DRectProj', 'texture3D', 'texture3DLod', 'texture3DProj',
'texture3DProjLod', 'textureCube', 'textureCubeGradEXT', 'textureCubeLod',
'textureCubeLodEXT', 'textureGather', 'textureGatherOffset',
'textureGatherOffsets', 'textureGrad', 'textureGradOffset', 'textureLod',
'textureLodOffset', 'textureOffset', 'textureProj', 'textureProjGrad',
'textureProjGradOffset', 'textureProjLod', 'textureProjLodOffset',
'textureProjOffset', 'textureQueryLevels', 'textureQueryLod',
'textureSamples', 'textureSize', 'textureVideoWEBGL', 'transpose', 'true',
'trunc', 'uaddCarry', 'uimage2D', 'uint', 'uintBitsToFloat', 'umulExtended',
'undef', 'uniform', 'unpackDouble2x32', 'unpackHalf2x16', 'unpackSnorm2x16',
'unpackSnorm4x8', 'unpackUnorm2x16', 'unpackUnorm4x8', 'usampler2D',
'usampler2DArray', 'usampler3D', 'usamplerCube', 'usubBorrow',
'usubpassInput', 'utexture2D', 'uvec2', 'uvec3', 'uvec4', 'vec2', 'vec3',
'vec4', 'void', 'volatile', 'while', 'yuv_2_rgb'
}
# rgba and stpq get rewritten to xyzw, so we only need to check xyzw here. This way we can keep
# renaming to names like, e.g., "rg".
xyzw_pattern = re.compile(r"^[xyzw]{1,4}$")
# HLSL registers base names can't be overwritten by macro arguments if token pasting (e.g. t##IDX).
hlsl_register_base_names = ['t', 's', 'u', 'b']
# HLSL registers (e.g., t0, u1) can't be overwritten by a #define.
hlsl_register_pattern = re.compile(r"^[tsub]\d+$")
# can we rename to or from 'name'?
def is_reserved_keyword(name):
return name in glsl_reserved\
or xyzw_pattern.match(name)\
or name in hlsl_register_base_names\
or hlsl_register_pattern.match(name)\
or name.startswith("$")\
or name.startswith("gl_")\
or name.startswith("GL_")\
or name.startswith("__pixel_local")\
or name.endswith("ANGLE")
def remove_leading_annotation(name):
if name[0] == '@':
# A leading '@' indicates identifier names that should be exported. Rename '@my_var' to
# '_EXPORTED_my_var' to enforce that '@my_var' and 'my_var' are not interchangeable.
return 'EXPORTED_' + name[1:]
if name[0] == '$':
# A leading '$' indicates identifier names that should not be renamed.
return name[1:]
return name
# Generates new identifier names to rewrite our variables.
class NameGenerator:
def __init__(self, first_letter_chars, additional_letter_chars):
self.first_letter_chars = first_letter_chars
self.additional_letter_chars = additional_letter_chars
self.name_index = 0
def next_name(self):
i = self.name_index
# Generate the first character from 'self.first_letter_chars'
name = self.first_letter_chars[i % len(self.first_letter_chars)]
i = i // len(self.first_letter_chars)
while i > 0:
# Generate the remaining characters from 'self.additional_letter_chars'
name += self.additional_letter_chars[i % len(self.additional_letter_chars)]
i = i // len(self.additional_letter_chars)
self.name_index = self.name_index + 1
return name
# Exported variables only use upper case letters in their names. HLSL semantics are not case
# sensitive and may also assign special meaning to numbers.
upper_case_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
upper_case_name_generator = NameGenerator(upper_case_chars, upper_case_chars + "_")
# Don't begin new names with the the '_' character. Internal code can begin names with '_' without
# fear of renaming collisions.
lower_and_upper_chars = "abcdefghijklmnopqrstuvwxyz" + upper_case_chars
general_name_generator = NameGenerator(lower_and_upper_chars, "_0123456789" + lower_and_upper_chars)
used_new_names = set()
def generate_new_name(*, force_upper_case):
name_generator = upper_case_name_generator if force_upper_case else general_name_generator
while True:
name = name_generator.next_name()
if not is_reserved_keyword(name) and not name in used_new_names:
used_new_names.add(name)
return name
# mapping from original identifiers to new names.
new_names = {}
def generate_new_names():
for name,count in sorted(all_id_counts.items(), key=lambda x:x[1], reverse=True):
new_name = (remove_leading_annotation(name)
if args.human_readable or is_reserved_keyword(name)
# HLSL semantics are not case sensitive and can assign special meaning to
# numbers. Make all exported names upper case with no numbers.
else generate_new_name(force_upper_case=name[0] == '@'))
new_names[name] = new_name
# used to rewrite rgba and stpq swizzles to xyzw.
rgba_stpq_pattern = re.compile(r"^([rgba]{1,4}|[stpq]{1,4})$")
rgba_stpq_remap = {'r':'x', 'g':'y', 'b':'z', 'a':'w',
's':'x', 't':'y', 'p':'z', 'q':'w',}
# minifies a single GLSL file.
class Minifier:
def __init__(self, code, basename, exports=set()):
# parse tokens.
lexer = lex.lex()
lexer.exports = exports
lexer.input(code)
self.tokens = [tok for tok in lexer];
self.exports = exports
self.basename = basename
# Strips unneeded code from the tokens. Called after all Minifiers have been parsed.
def strip_tokens(self):
assert(not args.human_readable)
# strip comments.
self.tokens = \
[tok for tok in self.tokens if "COMMENT" not in tok.type]
# strip unused defines.
self.tokens = [tok for tok in self.tokens if tok.type != "DEFINE"\
or all_id_reference_counts[tok.define_id] > 0]
# merge whitespace.
unmerged,self.tokens = self.tokens,[]
for tok in unmerged:
if tok.type == "DEFINE":
if tok.define_arglist != None:
tok.define_arglist.strip_tokens()
if tok.define_val != None:
tok.define_val.strip_tokens()
if tok.type == "DIRECTIVE":
if tok.directive_val != None:
tok.directive_val.strip_tokens()
if (tok.type == "WHITESPACE"
and len(self.tokens) > 0
and self.tokens[-1].type == "WHITESPACE"):
self.tokens[-1].value += tok.value
else:
self.tokens.append(tok)
# generates rewritten glsl from our tokens.
def emit_tokens_to_rewritten_glsl(self, out, *, preserve_exported_switches, calling_token_type:str=None):
# stand-in for a null token.
lasttoken = lambda : None
lasttoken.type = ""
lasttoken.value = ""
lasttoken_needs_whitespace = False
is_newline = True
for tok in self.tokens:
if tok.type == "WHITESPACE":
if args.human_readable:
out.write(tok.value)
is_newline = tok.value[-1] == '\n'
lasttoken_needs_whitespace = False
continue
is_directive = tok.type in ["DEFINE", "IFDEF", "DIRECTIVE"]
needs_whitespace = tok.type in ["FLOAT", "INT", "HEX", "ID", "DEFINED_ID"]
# Adding this calling_token_type != 'DEFINE' prevents us from adding a new line to stringify macros
if is_directive and not is_newline and calling_token_type != 'DEFINE':
out.write('\n')
elif needs_whitespace and lasttoken_needs_whitespace:
out.write(' ')
# is_newline will be false once we output the token (unless this value otherwise gets
# updated).
is_newline = False
if tok.type == "ID":
if (rgba_stpq_pattern.match(tok.value)
and lasttoken.type == "OP"
and lasttoken.value == "."):
# convert rgba and stpq to xyzw.
out.write(''.join([rgba_stpq_remap[ch] for ch in tok.value]))
else:
self.write_identifier(out, tok.value, preserve_exported_switches)
elif tok.type == "DEFINE":
out.write("#define ")
self.write_identifier(out, tok.define_id, preserve_exported_switches)
if tok.define_arglist != None:
is_newline = tok.define_arglist.emit_tokens_to_rewritten_glsl(\
out,\
preserve_exported_switches=preserve_exported_switches,\
calling_token_type=tok.type)
assert(not is_newline)
if tok.define_val != None:
out.write(' ')
is_newline = tok.define_val.emit_tokens_to_rewritten_glsl(\
out,\
preserve_exported_switches=preserve_exported_switches,\
calling_token_type=tok.type)
elif tok.type == "IFDEF":
out.write('#')
out.write(tok.ifdef_tag)
out.write(' ')
self.write_identifier(out, tok.ifdef_id, preserve_exported_switches)
elif tok.type == "DEFINED_ID":
out.write('defined(')
self.write_identifier(out, tok.defined_id, preserve_exported_switches)
out.write(')')
elif tok.type == "DIRECTIVE":
out.write("#")
if tok.directive_val != None:
is_newline = tok.directive_val.emit_tokens_to_rewritten_glsl(\
out,\
preserve_exported_switches=preserve_exported_switches,\
calling_token_type=tok.type)
else:
out.write(tok.value)
# Since we preserve whitespace in 'human_readable' mode, the newline after a
# preprocessor directive will happen for us automatically unless 'human_readable' is
# false.
if not args.human_readable and is_directive and not is_newline:
out.write('\n')
is_newline = True
lasttoken = tok
lasttoken_needs_whitespace = needs_whitespace
return is_newline
def write_identifier(self, out, identifier, preserve_exported_switches):
if preserve_exported_switches and identifier in exported_switches:
assert(identifier[0] == '@')
out.write(identifier[1:])
else:
out.write(new_names[identifier])
def write_exports(self, outdir):
output_path = os.path.join(outdir, f"{self.basename}.exports.h")
print(f"Exporting {output_path} <- {self.basename}")
out = open(output_path, "w", newline='\n')
out.write('#pragma once\n\n')
for exp in sorted(self.exports):
out.write('#define GLSL_%s "%s"\n' % (exp[1:], new_names[exp]))
out.write('#define GLSL_%s_raw %s\n' % (exp[1:], new_names[exp]))
out.close()
def write_embedded_glsl(self, outdir):
output_path = os.path.join(outdir, self.basename + ".hpp")
print("Embedding %s <- %s" % (output_path, self.basename))
out = open(output_path, "w", newline='\n')
out.write("#pragma once\n\n")
out.write(f'#include "{self.basename}.exports.h"\n\n')
# emit shader code.
root, ext = os.path.splitext(self.basename)
cpp_name = root
if ext != '.glsl':
cpp_name = f"{root}_{ext[1:]}"
out.write("namespace rive {\n")
out.write("namespace gpu {\n")
out.write("namespace glsl {\n")
if args.msvc:
# MSVC cannot compile raw shaders as strings because of an internal
# string length limit. There is, however, no limit on arrays so
# instead write it out as an array of individual characters.
out.write(f'const char {cpp_name}[] = {{\n ')
codeIO = io.StringIO()
self.emit_tokens_to_rewritten_glsl(codeIO, preserve_exported_switches=False)
out_ch_count = 0
for ch in codeIO.getvalue():
# use `repr` to escape the characters (but we need to handle single
# quote manually, since it will represent that as "'" - which will
# definitely not compile)
out.write(f" {repr(ch)},") if ch != "'" else out.write(" '\\'',")
out_ch_count = out_ch_count + 1
CHAR_COUNT_PER_LINE = 12
if (out_ch_count % CHAR_COUNT_PER_LINE) == 0:
out.write('\n ')
# Null-terminate so the array is a valid C-string when passed to
# APIs like ostream::operator<<(const char*), which read until '\0'.
out.write(" '\\0'\n};\n")
else:
# For non-MSVC outputs just output as a raw string
out.write(f'const char {cpp_name}[] = R"===(')
is_newline = self.emit_tokens_to_rewritten_glsl(out, preserve_exported_switches=False)
if not is_newline:
out.write('\n')
out.write(')===";\n')
out.write("} // namespace glsl\n")
out.write("} // namespace gpu\n")
out.write("} // namespace rive")
out.close()
def write_offline_glsl(self, outdir):
root, ext = os.path.splitext(self.basename)
output_path = os.path.join(outdir, f"{root}.minified{ext}")
print(f"Minifying f{output_path} <- {self.basename}")
out = open(output_path, "w", newline='\n')
self.emit_tokens_to_rewritten_glsl(out, preserve_exported_switches=True)
out.close()
# parse all GLSL files before renaming. This keeps the renaming consistent across files.
minifiers = [Minifier(open(f).read(), os.path.basename(f)) for f in args.files]
generate_new_names()
# minify all GLSL files.
if not os.path.exists(args.outdir):
os.makedirs(args.outdir)
for m in minifiers:
if not args.human_readable:
m.strip_tokens()
m.write_exports(args.outdir)
m.write_embedded_glsl(args.outdir)
m.write_offline_glsl(args.outdir)