blob: 44800a1e18f0b67941a7307c64fe0999f2b26498 [file] [log] [blame]
#!/usr/bin/python
import optparse
import os
import re
import subprocess
import sys
# This script takes an o3d cg shader from standard input and does the following:
#
# * it extracts entry points to vertex and fragment shaders as specified by
# VertexShaderEntryPoint and PixelShaderEntryPoint instructions;
#
# * renames NORMAL, TANGENT{,1} and BINORMAL{,1} attributes to ATTR8-12;
#
# * runs cgc with glslv and glslf profiles with those entry points;
#
# * renames attributes and uniforms back to their orignal names;
#
# * changes 'uniform vecN var[N]' to 'uniform matN var';
#
# * renames gl_Vertex and gl_MultiTexCoordN to position and texCoordN
# respectively and adds attribute declarations;
#
# * prints the results to standard output, separating them with SplitMarker
# instruction and keeping the MatrixLoadOrder instruction as is.
# Cygwin lies about the OS name ("posix" instead of "nt"), the line
# separator, and perhaps other things. For most robust behavior, try
# to find cgc on disk.
def find_o3d_root():
path = os.path.abspath(sys.path[0])
for i in range(5):
path = os.path.dirname(path)
if (os.path.isdir(os.path.join(path, 'o3d')) and
os.path.isdir(os.path.join(path, 'third_party'))):
return path
return ''
def default_cgc():
paths = ['/usr/bin/cgc',
'C:/Program Files/NVIDIA Corporation/Cg/bin/cgc.exe',
'C:/Program Files (x86)/NVIDIA Corporation/Cg/bin/cgc.exe']
for path in paths:
if os.path.exists(path):
return path
script_path = os.path.abspath(sys.path[0])
# Try again looking in the current working directory to match
# the layout of the prebuilt o3dConverter binaries.
cur_dir_paths = [os.path.join(script_path, 'cgc'),
os.path.join(script_path, 'cgc.exe')]
for path in cur_dir_paths:
if (os.path.exists(path)):
return path
# Last fallback is to use the binaries in o3d/third_party/cg/files.
# Unfortunately, because we can't rely on the OS name, we have to
# actually try running the cgc executable.
o3d_root = find_o3d_root();
cg_root = os.path.join(o3d_root, 'third_party', 'cg', 'files')
exe_paths = ['linux/bin/cgc',
'linux/bin64/cgc',
'mac/bin/cgc',
'win/bin/cgc.exe']
for exe_path in exe_paths:
try:
exe = os.path.join(cg_root, exe_path)
return_code = subprocess.call([exe, '-v'],
stdout=open(os.devnull, 'w'),
stderr=open(os.devnull, 'w'))
if return_code == 0 or return_code == 1:
return exe
except:
pass
# We don't know where cgc lives.
return ''
def check_cgc(cgc_path):
if not os.path.exists(cgc_path):
print >>sys.stderr, (cgc_path +
' is not found, use --cgc option to specify its')
print >>sys.stderr, 'location. You may need to install nvidia cg toolkit.'
sys.exit(1)
# cgc complains about TANGENT1 and BINORMAL1 semantics, so we hack it by
# replacing standard semantics with ATTR8-ATTR12 and then renaming them back to
# their original names.
ATTRIBUTES_TO_SEMANTICS = dict(
attr8 = 'normal',
attr9 = 'tangent',
attr10 = 'binormal',
attr11 = 'tangent1',
attr12 = 'binormal1')
MATRIX_UNIFORM_NAMES = [
'world',
'view',
'projection',
'worldView',
'worldViewProjection',
'worldInverse',
'viewInverse',
'projectionInverse',
'worldViewInverse',
'viewProjectionInverse',
'worldViewProjectionInverse',
'worldTranspose',
'viewTranspose',
'projectionTranspose',
'worldViewTranspose',
'viewProjectionTranspose',
'worldViewProjectionTranspose',
'worldInverseTranspose',
'viewInverseTranspose',
'projectionInverseTranspose',
'worldViewInverseTranspose',
'viewProjectionInverseTranspose',
'worldViewProjectionInverseTranspose'
]
MATRIX_UNIFORM_NAMES_MAPPING = dict((name.lower(), name)
for name in MATRIX_UNIFORM_NAMES)
def correct_semantic_case(name):
lower_name = name.lower()
return MATRIX_UNIFORM_NAMES_MAPPING.get(lower_name, lower_name)
def get_input_mapping(header):
ret = dict()
for l in header.splitlines():
if not l.startswith('//var'):
continue
old_name_and_type, semantic, new_name, _, _ = l.split(' : ')
if '[' in new_name:
new_name = new_name[:new_name.index('[')]
if new_name.startswith('$'):
new_name = new_name[1:]
if semantic:
ret[new_name] = correct_semantic_case(semantic)
else:
ret[new_name] = old_name_and_type.split(' ')[2]
return ret
def fix_glsl_body(body, input_mapping):
# Change uniform names back to original.
for match in re.findall(r'(?m)^uniform (?:\w+) (\w+)', body):
body = re.sub(r'\b%s\b' % match, input_mapping[match], body)
# Change attribute names back to original.
for match in re.findall(r'(?m)^attribute (?:\w+) (\w+)', body):
attr_name = input_mapping[match]
assert attr_name.startswith('$vin.')
orig_name = ATTRIBUTES_TO_SEMANTICS[attr_name[len('$vin.'):]]
body = re.sub(r'\b%s\b' % match, orig_name, body)
# Change vecN blah[N]; to matN blah;
body = re.sub(r'(?m)^uniform vec(\d) (\w+)\[\1\];', r'uniform mat\1 \2;',
body)
attributes = []
if 'gl_Vertex' in body:
# Change gl_Vertex to position and add attribute declaration.
body = re.sub(r'\bgl_Vertex\b', 'position', body)
attributes.append('attribute vec4 position;')
for n in xrange(8):
if 'gl_MultiTexCoord%d' % n in body:
# Change gl_MultiTexCoordN (0<=N<=7) to texCoordN and add attribute
# declaration.
body = re.sub(r'\bgl_MultiTexCoord%d\b' % n, 'texCoord%d' % n, body)
attributes.append('attribute vec4 texCoord%d;' % n)
# ATTRIBUTES_TO_SEMANTICS should have taken care of normals.
assert 'gl_Normal' not in body
if 'gl_Position' in body:
# If there is exactly one assignment to gl_Position, modify it similar to
# how RewriteVertexProgramSource in gl/effect_gl.cc does it. The input is
# taken from vec4 dx_clipping uniform.
#
# If there is more than one gl_Position mentioned in the shader, the
# converter fails.
assert len(re.findall('gl_Position', body)) == 1
attributes.append('vec4 _glPositionTemp;')
attributes.append('uniform vec4 dx_clipping;')
body = re.sub(r'\bgl_Position\b([^;]*);',
r'_glPositionTemp\1; gl_Position = vec4(' +
r'_glPositionTemp.x + _glPositionTemp.w * dx_clipping.x, ' +
r'dx_clipping.w * ' +
r'(_glPositionTemp.y + _glPositionTemp.w * dx_clipping.y), ' +
r'_glPositionTemp.z * 2.0 - _glPositionTemp.w, ' +
r'_glPositionTemp.w);', body)
return '\n'.join(attributes) + '\n\n' + body
def fix_glsl(glsl_shader):
# Hack for Cygwin lying about os.linesep and being POSIX on Windows.
if '\r\n' in glsl_shader:
header, body = re.split('\r\n'*2, glsl_shader, 1)
else:
header, body = re.split(os.linesep*2, glsl_shader, 1)
for l in header.splitlines():
assert l.startswith('//')
input_mapping = get_input_mapping(header)
return header + '\n\n' + fix_glsl_body(body, input_mapping)
def cg_rename_attributes(cg_shader):
for new, old in ATTRIBUTES_TO_SEMANTICS.iteritems():
cg_shader = re.sub(r'\b%s\b' % old.upper(), new.upper(), cg_shader)
return cg_shader
def cg_to_glsl(cg_shader, cgc_path):
cg_shader = cg_rename_attributes(cg_shader)
vertex_entry = re.search(r'#o3d\s+VertexShaderEntryPoint\s+(\w+)',
cg_shader).group(1)
p = subprocess.Popen([cgc_path]+('-profile glslv -entry %s' %
vertex_entry).split(' '),
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
glsl_vertex, err_v = p.communicate(cg_shader)
fragment_entry = re.search(r'#o3d\s+PixelShaderEntryPoint\s+(\w+)',
cg_shader).group(1)
p = subprocess.Popen([cgc_path]+('-profile glslf -entry %s' %
fragment_entry).split(' '),
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
glsl_fragment, err_f = p.communicate(cg_shader)
log = (
'// glslv profile log:\n' +
'\n'.join('// ' + l for l in err_v.splitlines()) + '\n\n'
'// glslf profile log:\n' +
'\n'.join('// ' + l for l in err_f.splitlines())) + '\n'
return glsl_vertex, glsl_fragment, log
def get_matrixloadorder(cg_shader):
return re.search(r'(?m)^.*#o3d\s+MatrixLoadOrder\b.*$', cg_shader).group(0)
def main(cg_shader, cgc_path):
matrixloadorder = get_matrixloadorder(cg_shader)
glsl_vertex, glsl_fragment, log = cg_to_glsl(cg_shader, cgc_path)
print log
print fix_glsl(glsl_vertex)
print
print '// #o3d SplitMarker'
print get_matrixloadorder(cg_shader).strip()
print
print fix_glsl(glsl_fragment)
if __name__ == '__main__':
cgc_path = default_cgc()
cmdline_parser = optparse.OptionParser()
cmdline_parser.add_option('-i', dest='file', default=None,
help='input shader; standard input if omitted')
cmdline_parser.add_option('--cgc', dest='cgc_path', default=cgc_path,
help='path to cgc [default: %default]')
options, args = cmdline_parser.parse_args()
cgc_path = options.cgc_path
check_cgc(cgc_path)
try:
if options.file is None:
f = sys.stdin
else:
f = open(options.file)
input = f.read()
except KeyboardInterrupt:
input = None
if not input:
cmdline_parser.print_help()
else:
main(input, cgc_path)