├── .gitignore ├── .tm_properties ├── support ├── version2.py ├── version.py ├── read_mxm_emitter.py ├── read_mxm_preview.py ├── material_preview_mxi.py ├── viewport_render_scene_settings.py ├── viewport_render_mxi.py ├── material_preview_scene.py ├── read_mxs_ref.py ├── read_mxs.py └── read_mxm.py ├── log.py ├── maths.py ├── readme.md ├── rfbin.py ├── tmpio.py ├── impmxs.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.pyc 4 | 5 | log.txt 6 | -------------------------------------------------------------------------------- /.tm_properties: -------------------------------------------------------------------------------- 1 | 2 | softTabs = true 3 | tabSize = 4 4 | scmStatus = true 5 | 6 | TM_PYTHON = "/Library/Frameworks/Python.framework/Versions/3.4/bin/python3" 7 | TM_GIT = "/usr/local/git/bin/git" 8 | 9 | excludeInFolderSearch = "{$excludeInFolderSearch,__pycache__}" 10 | excludeInBrowser = "{$excludeInBrowser,__pycache__,.tm_properties}" 11 | # excludeInBrowser = "{$excludeInBrowser}" 12 | excludeInFileChooser = "{$excludeInFileChooser,__pycache__,*.{c,so},.tm_properties}" 13 | excludeFilesInBrowser = "{$excludeFilesInBrowser,*.{c,so},.tm_properties}" 14 | # includeInBrowser = "{$includeInBrowser}" 15 | # includeInFileChooser = "{$includeInFileChooser}" 16 | -------------------------------------------------------------------------------- /support/version2.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import argparse 28 | import os 29 | 30 | 31 | if __name__ == "__main__": 32 | parser = argparse.ArgumentParser(description="pymaxwell version number", 33 | epilog='', 34 | formatter_class=argparse.RawDescriptionHelpFormatter, 35 | add_help=True, ) 36 | parser.add_argument('pymaxwell_path', type=str, help='path to pymaxwell') 37 | args = parser.parse_args() 38 | 39 | p = args.pymaxwell_path 40 | if(not os.path.exists(p)): 41 | sys.stderr.write("{}: No such file or directory".format(p)) 42 | sys.exit(1) 43 | sys.path.insert(0, p) 44 | try: 45 | import pymaxwell 46 | v = pymaxwell.getPyMaxwellVersion() 47 | sys.stdout.write("{}".format(v)) 48 | sys.exit(0) 49 | except ImportError as e: 50 | sys.stderr.write("{}".format(e)) 51 | sys.exit(1) 52 | -------------------------------------------------------------------------------- /support/version.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import os 31 | 32 | 33 | def log(msg, indent=0): 34 | m = "{0}> {1}".format(" " * indent, msg) 35 | print(m) 36 | 37 | 38 | def main(args): 39 | required = tuple([int(i) for i in args.version.split('.')]) 40 | s = pymaxwell.getPyMaxwellVersion() 41 | v = tuple([int(i) for i in s.split('.')]) 42 | if(v < required): 43 | # older version 44 | sys.exit(3) 45 | # ok 46 | sys.exit(0) 47 | 48 | 49 | if __name__ == "__main__": 50 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Check pymaxwell version number'''), epilog='', 51 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 52 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 53 | parser.add_argument('version', type=str, help='minimum required version string, as returned from pymaxwell.getPyMaxwellVersion()') 54 | args = parser.parse_args() 55 | 56 | PYMAXWELL_PATH = args.pymaxwell_path 57 | 58 | try: 59 | import pymaxwell 60 | except ImportError: 61 | if(not os.path.exists(PYMAXWELL_PATH)): 62 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 63 | sys.path.insert(0, PYMAXWELL_PATH) 64 | try: 65 | import pymaxwell 66 | except ImportError: 67 | # import error 68 | sys.exit(2) 69 | 70 | try: 71 | main(args) 72 | except Exception as e: 73 | m = traceback.format_exc() 74 | log(m) 75 | # unexpected error 76 | sys.exit(1) 77 | # ok 78 | sys.exit(0) 79 | -------------------------------------------------------------------------------- /support/read_mxm_emitter.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import os 31 | 32 | 33 | def log(msg, indent=0): 34 | m = "{0}> {1}".format(" " * indent, msg) 35 | print(m) 36 | 37 | 38 | def main(args): 39 | p = args.mxm_path 40 | s = Cmaxwell(mwcallback) 41 | m = s.readMaterial(p) 42 | for i in range(m.getNumLayers()[0]): 43 | l = m.getLayer(i) 44 | e = l.getEmitter() 45 | if(e.isNull()): 46 | # no emitter layer 47 | return False 48 | if(not e.getState()[0]): 49 | # there is, but is disabled 50 | return False 51 | # is emitter 52 | return True 53 | 54 | 55 | if __name__ == "__main__": 56 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Read MXM file and check is there is an emitter layer'''), epilog='', 57 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 58 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 59 | parser.add_argument('mxm_path', type=str, help='path to .mxm') 60 | args = parser.parse_args() 61 | 62 | PYMAXWELL_PATH = args.pymaxwell_path 63 | 64 | try: 65 | from pymaxwell import * 66 | except ImportError: 67 | if(not os.path.exists(PYMAXWELL_PATH)): 68 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 69 | sys.path.insert(0, PYMAXWELL_PATH) 70 | from pymaxwell import * 71 | 72 | found = False 73 | try: 74 | found = main(args) 75 | except Exception as e: 76 | m = traceback.format_exc() 77 | log(m) 78 | sys.exit(1) 79 | if(found): 80 | # lets communicate with exit code 81 | sys.exit(100) 82 | sys.exit(0) 83 | -------------------------------------------------------------------------------- /support/read_mxm_preview.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import os 31 | 32 | 33 | def log(msg, indent=0): 34 | m = "{0}> {1}".format(" " * indent, msg) 35 | print(m) 36 | 37 | 38 | def main(args): 39 | p = args.mxm_path 40 | s = Cmaxwell(mwcallback) 41 | m = s.readMaterial(p) 42 | try: 43 | a, ok = m.getPreview() 44 | except: 45 | ok = False 46 | if(ok): 47 | sd = os.path.split(os.path.realpath(__file__))[0] 48 | mn = os.path.split(p)[1] 49 | np = os.path.join(sd, "{}.npy".format(mn)) 50 | # TODO: save this somewhere else, use the same rules as with other temp files (ie utils.tmp_dir) 51 | numpy.save(np, a) 52 | 53 | 54 | if __name__ == "__main__": 55 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Read preview image from MXM file'''), epilog='', 56 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 57 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 58 | parser.add_argument('numpy_path', type=str, help='path to directory containing numpy') 59 | parser.add_argument('mxm_path', type=str, help='path to .mxm') 60 | args = parser.parse_args() 61 | 62 | PYMAXWELL_PATH = args.pymaxwell_path 63 | NUMPY_PATH = args.numpy_path 64 | 65 | try: 66 | from pymaxwell import * 67 | except ImportError: 68 | if(not os.path.exists(PYMAXWELL_PATH)): 69 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 70 | sys.path.insert(0, PYMAXWELL_PATH) 71 | from pymaxwell import * 72 | 73 | try: 74 | import numpy 75 | except ImportError: 76 | sys.path.insert(0, NUMPY_PATH) 77 | import numpy 78 | 79 | try: 80 | main(args) 81 | except Exception as e: 82 | m = traceback.format_exc() 83 | log(m) 84 | sys.exit(1) 85 | sys.exit(0) 86 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import os 20 | import re 21 | import platform 22 | 23 | 24 | LOG_FILE_PATH = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'log.txt')) 25 | LOG_CONVERT = re.compile("\033\[[0-9;]+m") 26 | NUMBER_OF_WARNINGS = 0 27 | 28 | 29 | def clear_log(): 30 | global NUMBER_OF_WARNINGS 31 | NUMBER_OF_WARNINGS = 0 32 | with open(LOG_FILE_PATH, mode='w', encoding='utf-8', ): 33 | # clear log file.. 34 | pass 35 | 36 | 37 | clear_log() 38 | 39 | 40 | def copy_paste_log(log_file_path): 41 | import shutil 42 | shutil.copyfile(LOG_FILE_PATH, log_file_path) 43 | 44 | 45 | class LogStyles: 46 | NORMAL = "\033[40m\033[32m" 47 | HEADER = "\033[46m\033[30m" 48 | MESSAGE = "\033[42m\033[30m" 49 | WARNING = "\033[43m\033[1;30mWARNING: " 50 | ERROR = "\033[41m\033[1;30mERROR: " 51 | END = "\033[0m" 52 | EOL = "\n" 53 | 54 | 55 | if(platform.system() == 'Windows'): 56 | # no pretty logging for windows 57 | LogStyles.NORMAL = "" 58 | LogStyles.HEADER = "" 59 | LogStyles.MESSAGE = "" 60 | LogStyles.WARNING = "WARNING: " 61 | LogStyles.ERROR = "ERROR: " 62 | LogStyles.END = "" 63 | 64 | 65 | def log(msg="", indent=0, style=LogStyles.NORMAL, instance=None, prefix="> ", ): 66 | global NUMBER_OF_WARNINGS 67 | if(style == LogStyles.WARNING): 68 | NUMBER_OF_WARNINGS += 1 69 | 70 | if(instance is None): 71 | inst = "" 72 | else: 73 | cn = instance.__class__.__name__ 74 | cl = cn.split(".") 75 | inst = "{0}: ".format(cl[-1]) 76 | 77 | m = "{0}{1}{2}{3}{4}{5}".format(" " * indent, style, prefix, inst, msg, LogStyles.END) 78 | 79 | print(m) 80 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 81 | f.write("{}{}".format(re.sub(LOG_CONVERT, '', m), LogStyles.EOL)) 82 | 83 | 84 | def log_args(locals, self, header="arguments: ", indent=1, style=LogStyles.NORMAL, prefix="> ", ): 85 | import inspect 86 | l = dict([(k, v) for k, v in locals.items() if v != self and k != '__class__']) 87 | f = [i for i in inspect.getfullargspec(self.__init__).args if i != 'self'] 88 | t = " " 89 | s = " " 90 | hl = 0 91 | for i in f: 92 | if(len(i) > hl): 93 | hl = len(i) 94 | # hl += 1 95 | vs = ["{0}: {1}".format(i.ljust(hl, s), l[i]) for i in f] 96 | for i, v in enumerate(vs): 97 | if(i == 0): 98 | vs[i] = "{0}{1}\n".format(header, v) 99 | elif(i == len(vs) - 1): 100 | vs[i] = "{0}{1}{2}{3}".format(t * indent, s * len(prefix), s * len(header), v) 101 | else: 102 | vs[i] = "{0}{1}{2}{3}\n".format(t * indent, s * len(prefix), s * len(header), v) 103 | log("".join(vs), indent, style, None, prefix, ) 104 | -------------------------------------------------------------------------------- /support/material_preview_mxi.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import json 31 | import os 32 | import shlex 33 | import subprocess 34 | 35 | 36 | LOG_FILE_PATH = None 37 | 38 | 39 | def log(msg, indent=0): 40 | m = "{0}> {1}".format(" " * indent, msg) 41 | print(m) 42 | if(LOG_FILE_PATH is not None): 43 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 44 | f.write("{}{}".format(m, "\n")) 45 | 46 | 47 | def main(args): 48 | mp = os.path.join(args.directory, 'render.mxi') 49 | ep = os.path.join(args.directory, 'render.exr') 50 | a = numpy.zeros((1, 1, 3), dtype=numpy.float, ) 51 | if(os.path.exists(mp)): 52 | log('reading mxi: {}'.format(mp), 2) 53 | i = CmaxwellMxi() 54 | i.read(mp) 55 | a, _ = i.getRenderBuffer(32) 56 | elif(os.path.exists(ep)): 57 | log('reading exr: {}'.format(ep), 2) 58 | i = CmaxwellMxi() 59 | i.readImage(ep) 60 | i.write(mp) 61 | a, _ = i.getRenderBuffer(32) 62 | else: 63 | log('image not found..', 2) 64 | 65 | np = os.path.join(args.directory, "preview.npy") 66 | log('writing numpy: {}'.format(np), 2) 67 | numpy.save(np, a) 68 | 69 | 70 | if __name__ == "__main__": 71 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Make Maxwell Material from serialized data'''), epilog='', 72 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 73 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 74 | parser.add_argument('numpy_path', type=str, help='path to directory containing numpy') 75 | parser.add_argument('log_file', type=str, help='path to log file') 76 | parser.add_argument('directory', type=str, help='path to temp directory') 77 | args = parser.parse_args() 78 | 79 | PYMAXWELL_PATH = args.pymaxwell_path 80 | NUMPY_PATH = args.numpy_path 81 | 82 | try: 83 | from pymaxwell import * 84 | except ImportError: 85 | if(not os.path.exists(PYMAXWELL_PATH)): 86 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 87 | sys.path.insert(0, PYMAXWELL_PATH) 88 | from pymaxwell import * 89 | 90 | try: 91 | import numpy 92 | except ImportError: 93 | sys.path.insert(0, NUMPY_PATH) 94 | import numpy 95 | 96 | LOG_FILE_PATH = args.log_file 97 | 98 | try: 99 | main(args) 100 | except Exception as e: 101 | import traceback 102 | m = traceback.format_exc() 103 | log(m) 104 | sys.exit(1) 105 | sys.exit(0) 106 | -------------------------------------------------------------------------------- /support/viewport_render_scene_settings.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import json 31 | import os 32 | 33 | 34 | LOG_FILE_PATH = None 35 | 36 | 37 | def log(msg, indent=0): 38 | m = "{0}> {1}".format(" " * indent, msg) 39 | print(m) 40 | if(LOG_FILE_PATH is not None): 41 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 42 | f.write("{}{}".format(m, "\n")) 43 | 44 | 45 | def main(args): 46 | s = Cmaxwell(mwcallback) 47 | p = os.path.join(args.directory, "scene.mxs") 48 | log('reading scene: {}'.format(p), 2) 49 | ok = s.readMXS(p) 50 | if(not ok): 51 | sys.exit(1) 52 | 53 | log('setting parameters..', 2) 54 | # if draft engine is selected, no mxi will be created.. now what.. 55 | s.setRenderParameter('ENGINE', args.quality) 56 | 57 | mxi = os.path.join(args.directory, "render.mxi") 58 | s.setRenderParameter('MXI FULLNAME', mxi) 59 | 60 | exr = os.path.join(args.directory, "render.exr") 61 | s.setPath('RENDER', exr, 32) 62 | 63 | s.setRenderParameter('DO NOT SAVE MXI FILE', False) 64 | s.setRenderParameter('DO NOT SAVE IMAGE FILE', False) 65 | 66 | # turn off channels 67 | s.setRenderParameter('EMBED CHANNELS', 1) 68 | ls = ['DO ALPHA CHANNEL', 'DO IDOBJECT CHANNEL', 'DO IDMATERIAL CHANNEL', 'DO SHADOW PASS CHANNEL', 'DO MOTION CHANNEL', 69 | 'DO ROUGHNESS CHANNEL', 'DO FRESNEL CHANNEL', 'DO NORMALS CHANNEL', 'DO POSITION CHANNEL', 'DO ZBUFFER CHANNEL', 70 | 'DO DEEP CHANNEL', 'DO UV CHANNEL', 'DO ALPHA CUSTOM CHANNEL', 'DO REFLECTANCE CHANNEL', ] 71 | for n in ls: 72 | s.setRenderParameter(n, 0) 73 | 74 | log('writing scene: {}'.format(p), 2) 75 | ok = s.writeMXS(p) 76 | if(not ok): 77 | sys.exit(1) 78 | log('done.', 2) 79 | 80 | 81 | if __name__ == "__main__": 82 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Modify viewport render scene settings'''), epilog='', 83 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 84 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 85 | parser.add_argument('log_file', type=str, help='path to log file') 86 | parser.add_argument('directory', type=str, help='path to temp directory') 87 | parser.add_argument('quality', type=str, help='quality') 88 | args = parser.parse_args() 89 | 90 | PYMAXWELL_PATH = args.pymaxwell_path 91 | 92 | try: 93 | from pymaxwell import * 94 | except ImportError: 95 | if(not os.path.exists(PYMAXWELL_PATH)): 96 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 97 | sys.path.insert(0, PYMAXWELL_PATH) 98 | from pymaxwell import * 99 | 100 | LOG_FILE_PATH = args.log_file 101 | 102 | try: 103 | main(args) 104 | except Exception as e: 105 | import traceback 106 | m = traceback.format_exc() 107 | log(m) 108 | sys.exit(1) 109 | sys.exit(0) 110 | -------------------------------------------------------------------------------- /support/viewport_render_mxi.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import json 31 | import os 32 | import shlex 33 | import subprocess 34 | 35 | 36 | LOG_FILE_PATH = None 37 | 38 | 39 | def log(msg, indent=0): 40 | m = "{0}> {1}".format(" " * indent, msg) 41 | print(m) 42 | if(LOG_FILE_PATH is not None): 43 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 44 | f.write("{}{}".format(m, "\n")) 45 | 46 | 47 | def main(args): 48 | ''' 49 | mp = os.path.join(args.directory, 'render.mxi') 50 | ep = os.path.join(args.directory, 'render.exr') 51 | a = numpy.zeros((1, 1, 3), dtype=numpy.float, ) 52 | if(os.path.exists(mp)): 53 | log('reading mxi: {}'.format(mp), 2) 54 | i = CmaxwellMxi() 55 | i.read(mp) 56 | a, _ = i.getRenderBuffer(32) 57 | elif(os.path.exists(ep)): 58 | log('reading exr: {}'.format(ep), 2) 59 | i = CmaxwellMxi() 60 | i.readImage(ep) 61 | i.write(mp) 62 | a, _ = i.getRenderBuffer(32) 63 | else: 64 | log('image not found..', 2) 65 | 66 | np = os.path.join(args.directory, "preview.npy") 67 | log('writing numpy: {}'.format(np), 2) 68 | numpy.save(np, a) 69 | ''' 70 | ep = os.path.join(args.directory, 'render2.exr') 71 | a = numpy.zeros((1, 1, 3), dtype=numpy.float, ) 72 | if(os.path.exists(ep)): 73 | log('reading exr: {}'.format(ep), 2) 74 | i = CmaxwellMxi() 75 | i.readImage(ep) 76 | # i.write(mp) 77 | a, _ = i.getRenderBuffer(32) 78 | else: 79 | log('image not found..', 2) 80 | np = os.path.join(args.directory, "preview.npy") 81 | log('writing numpy: {}'.format(np), 2) 82 | numpy.save(np, a) 83 | 84 | 85 | if __name__ == "__main__": 86 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Make Maxwell Material from serialized data'''), epilog='', 87 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 88 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 89 | parser.add_argument('numpy_path', type=str, help='path to directory containing numpy') 90 | parser.add_argument('log_file', type=str, help='path to log file') 91 | parser.add_argument('directory', type=str, help='path to temp directory') 92 | args = parser.parse_args() 93 | 94 | PYMAXWELL_PATH = args.pymaxwell_path 95 | NUMPY_PATH = args.numpy_path 96 | 97 | try: 98 | from pymaxwell import * 99 | except ImportError: 100 | if(not os.path.exists(PYMAXWELL_PATH)): 101 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 102 | sys.path.insert(0, PYMAXWELL_PATH) 103 | from pymaxwell import * 104 | 105 | try: 106 | import numpy 107 | except ImportError: 108 | sys.path.insert(0, NUMPY_PATH) 109 | import numpy 110 | 111 | LOG_FILE_PATH = args.log_file 112 | 113 | try: 114 | main(args) 115 | except Exception as e: 116 | import traceback 117 | m = traceback.format_exc() 118 | log(m) 119 | sys.exit(1) 120 | sys.exit(0) 121 | -------------------------------------------------------------------------------- /support/material_preview_scene.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import json 31 | import os 32 | 33 | 34 | LOG_FILE_PATH = None 35 | 36 | 37 | def log(msg, indent=0): 38 | m = "{0}> {1}".format(" " * indent, msg) 39 | print(m) 40 | if(LOG_FILE_PATH is not None): 41 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 42 | f.write("{}{}".format(m, "\n")) 43 | 44 | 45 | def main(args): 46 | s = Cmaxwell(mwcallback) 47 | log('reading scene: {}'.format(args.scene), 2) 48 | ok = s.readMXS(args.scene) 49 | if(not ok): 50 | sys.exit(1) 51 | 52 | def get_material_names(s): 53 | it = CmaxwellMaterialIterator() 54 | o = it.first(s) 55 | l = [] 56 | while not o.isNull(): 57 | name = o.getName() 58 | l.append(name) 59 | o = it.next() 60 | return l 61 | 62 | names = get_material_names(s) 63 | for n in names: 64 | if(n.lower() == 'preview'): 65 | break 66 | 67 | log('swapping material: {}'.format(n), 2) 68 | material = s.getMaterial(n) 69 | material.read(os.path.join(args.directory, 'material.mxm')) 70 | material.forceToWriteIntoScene() 71 | 72 | log('setting parameters..', 2) 73 | # if draft engine is selected, no mxi will be created.. now what.. 74 | s.setRenderParameter('ENGINE', args.quality) 75 | 76 | exr = os.path.join(args.directory, "render.exr") 77 | s.setPath('RENDER', exr, 32) 78 | 79 | s.setRenderParameter('DO NOT SAVE MXI FILE', False) 80 | s.setRenderParameter('DO NOT SAVE IMAGE FILE', False) 81 | 82 | src_dir, _ = os.path.split(args.scene) 83 | ok = s.addSearchingPath(src_dir) 84 | 85 | sp = os.path.join(args.directory, "scene.mxs") 86 | log('writing scene: {}'.format(sp), 2) 87 | ok = s.writeMXS(sp) 88 | if(not ok): 89 | sys.exit(1) 90 | log('done.', 2) 91 | 92 | 93 | if __name__ == "__main__": 94 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Modify material preview scene settings'''), epilog='', 95 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 96 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 97 | parser.add_argument('log_file', type=str, help='path to log file') 98 | parser.add_argument('scene', type=str, help='path to source scene mxs') 99 | parser.add_argument('directory', type=str, help='path to temp directory') 100 | parser.add_argument('quality', type=str, help='quality') 101 | args = parser.parse_args() 102 | 103 | PYMAXWELL_PATH = args.pymaxwell_path 104 | 105 | try: 106 | from pymaxwell import * 107 | except ImportError: 108 | if(not os.path.exists(PYMAXWELL_PATH)): 109 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 110 | sys.path.insert(0, PYMAXWELL_PATH) 111 | from pymaxwell import * 112 | 113 | LOG_FILE_PATH = args.log_file 114 | 115 | try: 116 | main(args) 117 | except Exception as e: 118 | import traceback 119 | m = traceback.format_exc() 120 | log(m) 121 | sys.exit(1) 122 | sys.exit(0) 123 | -------------------------------------------------------------------------------- /maths.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import math 20 | from mathutils import Matrix, Vector, Quaternion 21 | from bpy_extras import io_utils 22 | 23 | 24 | def normalize(v, vmin, vmax): 25 | """Fits a value between minimum and maximum.""" 26 | return (v - vmin) / (vmax - vmin) 27 | 28 | 29 | def interpolate(nv, vmin, vmax): 30 | """Fit normalized value to range between minumum and maximum.""" 31 | return vmin + (vmax - vmin) * nv 32 | 33 | 34 | def map(v, min1, max1, min2, max2): 35 | """Converts one range definded by value, minimum and maximum (min1, max1) to different range definded by minimum and maximum (min2, max2).""" 36 | # return interpolate(normalize(v, min1, max1), min2, max2) 37 | return remap(v, min1, max1, min2, max2) 38 | 39 | 40 | def remap(v, min1, max1, min2, max2): 41 | """Converts one range definded by value, minimum and maximum (min1, max1) to different range definded by minimum and maximum (min2, max2). 42 | Also this one solves a few errors, like value out of min1 - max1 range..""" 43 | v = clamp(v, min1, max1) 44 | r = interpolate(normalize(v, min1, max1), min2, max2) 45 | r = clamp(r, min2, max2) 46 | return r 47 | 48 | 49 | def clamp(v, vmin, vmax): 50 | """Clamp value between min and max.""" 51 | if(vmax <= vmin): 52 | raise ValueError("Maximum value is smaller than or equal to minimum.") 53 | if(v <= vmin): 54 | return vmin 55 | if(v >= vmax): 56 | return vmax 57 | return v 58 | 59 | 60 | def maprange(v, ar, br, ): 61 | # http://rosettacode.org/wiki/Map_range#Python 62 | (a1, a2), (b1, b2) = ar, br 63 | return b1 + ((v - a1) * (b2 - b1) / (a2 - a1)) 64 | 65 | 66 | def rotation_to(a, b): 67 | """Calculates shortest Quaternion from Vector a to Vector b""" 68 | # a - up vector 69 | # b - direction to point to 70 | 71 | # http://stackoverflow.com/questions/1171849/finding-quaternion-representing-the-rotation-from-one-vector-to-another 72 | # https://github.com/toji/gl-matrix/blob/f0583ef53e94bc7e78b78c8a24f09ed5e2f7a20c/src/gl-matrix/quat.js#L54 73 | 74 | a = a.normalized() 75 | b = b.normalized() 76 | q = Quaternion() 77 | 78 | tmpvec3 = Vector() 79 | xUnitVec3 = Vector((1, 0, 0)) 80 | yUnitVec3 = Vector((0, 1, 0)) 81 | 82 | dot = a.dot(b) 83 | if(dot < -0.999999): 84 | # tmpvec3 = cross(xUnitVec3, a) 85 | tmpvec3 = xUnitVec3.cross(a) 86 | if(tmpvec3.length < 0.000001): 87 | tmpvec3 = yUnitVec3.cross(a) 88 | tmpvec3.normalize() 89 | # q = Quaternion(tmpvec3, Math.PI) 90 | q = Quaternion(tmpvec3, math.pi) 91 | elif(dot > 0.999999): 92 | q.x = 0 93 | q.y = 0 94 | q.z = 0 95 | q.w = 1 96 | else: 97 | tmpvec3 = a.cross(b) 98 | q.x = tmpvec3[0] 99 | q.y = tmpvec3[1] 100 | q.z = tmpvec3[2] 101 | q.w = 1 + dot 102 | q.normalize() 103 | return q 104 | 105 | 106 | def eye_target_up_from_matrix(matrix, distance=1.0, ): 107 | eye = matrix.to_translation() 108 | target = matrix * Vector((0.0, 0.0, -distance)) 109 | up = matrix * Vector((0.0, 1.0, 0.0)) - eye 110 | return eye, target, up 111 | 112 | 113 | def shift_vert_along_normal(tco, tno, v): 114 | """Shifts Vector along Normal Vector""" 115 | co = Vector(tco) 116 | no = Vector(tno) 117 | return co + (no.normalized() * v) 118 | 119 | 120 | def distance_vectors(a, b, ): 121 | """Distance between two 3d Vectors""" 122 | return ((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2) ** 0.5 123 | 124 | 125 | def real_length_to_relative(matrix, length): 126 | """From matrix_world and desired real size length in meters, calculate relative length 127 | without matrix applied. Apply matrix, and you will get desired length.""" 128 | l, r, s = matrix.decompose() 129 | ms = Matrix.Scale(s.x, 4) 130 | l = Vector((length, 0, 0)) 131 | v = ms.inverted() * l 132 | return v.x 133 | 134 | 135 | def apply_matrix(points, matrix): 136 | matrot = matrix.decompose()[1].to_matrix().to_4x4() 137 | r = [None] * len(points) 138 | for i, p in enumerate(points): 139 | co = matrix * Vector((p[0], p[1], p[2])) 140 | no = matrot * Vector((p[3], p[4], p[5])) 141 | r[i] = (co.x, co.y, co.z, no.x, no.y, no.z, p[6], p[7], p[8]) 142 | return r 143 | 144 | 145 | def unapply_matrix(points, matrix): 146 | m = matrix.inverted() 147 | return apply_matrix(points, m) 148 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # **blendmaxwell** 2 | #### Maxwell Render 3 exporter for Blender 3 | 4 | ![teaser](https://raw.githubusercontent.com/uhlik/bpy/media/bmr2.jpg) 5 | 6 | ### notice 7 | 8 | This exporter addon is for **Maxwell Render 3** only, compatible with version 3.2.1.3 and later 9 | 10 | ### features: 11 | 12 | * Works on Mac OS X, Linux and Windows 13 | * Compatible with Maxwell Render 3 version 3.2.1.3+ and Blender 2.77a+ < 2.80 14 | * UI as close to Maxwell Studio as possible 15 | * All renderable geometry (except Metaballs) 16 | * Object hierarchy (not renderable objects are removed unless they have renderable child objects) 17 | * Mesh objects using the same mesh data (and renderable Curves) are exported as instances (optional) 18 | * MXS references with viewport preview 19 | * Dupli verts, faces and group 20 | * Multiple UV channels 21 | * Custom and Extension Material creation and editing inside Blender or with Mxed 22 | * Save and load Custom and Extension Materials from or to Blender material editor 23 | * Material assignment (including backface materials) and multiple materials per object 24 | * Extension Materials creation and editing inside Blender 25 | * Maxwell procedural textures 26 | * Material preview rendering 27 | * Viewport rendering (not interactive) 28 | * Cameras 29 | * Render parameters 30 | * All render channels including Custom Alphas 31 | * Environment parameters (Sun can be optionally set by Sun lamp) 32 | * Object parameters 33 | * Export objects and cameras for movement and deformation blur 34 | * Maxwell Extensions: Particles, Grass, Hair, Scatter, Subdivision, Sea, Cloner, Volumetrics 35 | * Export Subdivision modifiers if their type is Catmull-Clark and they are at the end of modifier stack on regular mesh objects (optional) 36 | * Scene import (objects, emitters, cameras and sun selectively) 37 | 38 | ![ui](https://raw.githubusercontent.com/uhlik/bpy/media/bmr.png) 39 | 40 | ### addon installation - Mac OS X: 41 | 42 | * Maxwell must be installed in usual place i.e. ```/Applications/Maxwell 3``` 43 | * Download python 3.5.1 from [https://www.python.org/downloads/release/python-351/](https://www.python.org/downloads/release/python-351/) and install 44 | * Download this repository clicking 'Download ZIP', extract, rename directory to ```blendmaxwell``` and put to ```~/Library/Application Support/Blender/*BLENDER_VERSION*/scripts/addons/``` 45 | * Start Blender, go to User Preferences > Add-ons, search for 'blendmaxwell' in Render category and enable it, then choose 'Maxwell Render' from render engines list in Info panel header 46 | 47 | ### addon installation - Windows: 48 | 49 | * Download this repository clicking 'Download ZIP', extract, rename directory to ```blendmaxwell``` and put to ```C:\Users\USERNAME\AppData\Roaming\Blender Foundation\Blender\*BLENDER_VERSION*\scripts\addons\``` 50 | * Start Blender, go to User Preferences > Add-ons, search for 'blendmaxwell' in Render category and enable it, then choose 'Maxwell Render' from render engines list in Info panel header 51 | 52 | ### addon installation - Linux: 53 | 54 | * Append this ```export LD_LIBRARY_PATH=$MAXWELL3_ROOT:$LD_LIBRARY_PATH``` to your .bashrc after generated stuff from Maxwell installation, i.e. after ```MAXWELL3_ROOT``` is exported 55 | * To fix complains of some extensions, install ```libtbb-dev```, but this step might be optional, it is used by extension not supported in addon 56 | * Download this repository clicking 'Download ZIP', extract, rename directory to ```blendmaxwell``` and put to ```~/.config/blender/*BLENDER_VERSION*/scripts/addons/``` 57 | * Start Blender from terminal, go to User Preferences > Add-ons, search for 'blendmaxwell' in Render category and enable it, then choose 'Maxwell Render' from render engines list in Info panel header 58 | 59 | #### installation notes: 60 | 61 | * In case of problem with presets (emitters, extension materials, ..., ), remove on **Mac OS X**: ```~/Library/Application Support/Blender/*BLENDER_VERSION*/scripts/presets/blendmaxwell```, on **Windows**: ```C:\Users\USERNAME\AppData\Roaming\Blender Foundation\Blender\*BLENDER_VERSION*\scripts\presets\blendmaxwell```, or on **Linux**: ```~/.config/blender/*BLENDER_VERSION*/scripts/presets/blendmaxwell``` and restart Blender. default presets will be recreated automatically. 62 | 63 | #### known issues: 64 | * Due to changes in Blender's triangulation operator, Maxwell Subdivision modifier is disabled. 65 | 66 | 67 | *** 68 | 69 | **changelog:** 70 | 71 | * 0.4.5 support for vertex-per-face normals, mesh auto-smooth, split normals etc., added text overlay, disabled subdivision modifier until fixed 72 | * 0.4.4 material preview and viewport (not interactive) rendering, mxs import with mxs references 73 | * 0.4.3 critical fix: smooth faces export, experimental feature: material preview rendering (currently Mac OS X only) 74 | * 0.4.2 movement and deformation blur, faster mesh export 75 | * 0.4.1 procedural textures, faster reading of mxs references 76 | * 0.4.0 heavy refactoring, added mxs reference viewport preview 77 | * 0.3.9 colors exported in 32 bits and as shown in blender (gamma correction), added grass modifier presets, added displacement in extension materials, added stereo cameras (maxwell 3.2), added realflow particles bin export operator (file > export menu), updated material presets (maxwell 3.2.1.0), fixed cleaning of mesh datablocks created during export, fixed exporting of fake user materials, fixed export of particles on meshes without polygons (and a lot of small fixes and ui tweaks) 78 | * 0.3.8 custom alphas for objects and materials, many ui improvements, particle object/group instances now exports correctly when base objects are hidden, addon preferences for automatic type selection for new material, environment and particles, changed preset location, setting camera to ortho now changes viewport to ortho 79 | * 0.3.7 custom material editor, custom material import/export 80 | * 0.3.6 added: export particle uvs, camera lock exposure, choosing external materials with mxed in browser mode, choose scene for auto preview in mxed, fixed: import mxs: object transformation 81 | * 0.3.5 particle object/group instances, quick setting object properties/object id to multiple objects, blocked emitters, many fixes (reading/drawing material previews, missing cloner objects, hair uvs, ...) 82 | * 0.3.4 hair with children particles root uvs (requires blender 2.76), wire export faster and with smaller files, fixes here and there 83 | * 0.3.3 simplified installation procedure on Mac OS X, pymaxwell is now imported directly from ```/Applications/Maxwell 3```, also fixed some bugs.. 84 | * 0.3.2 Maxwell 3.2 update, includes majority of new features: material priority, saving to psd, reflection and refraction channels, reflectance channel, scatter and grass updates. also wireframe and auto-subdivision export is restored, added basic progress reporting, hair uvs, hair extension material and many more small fixes and tweaks 85 | * 0.3.1 last version working with Maxwell 3.1 (with a few bugs): [ddbad692a25c6e6e72d11092d8f063f6ed1d048e](https://github.com/uhlik/blendmaxwell/tree/ddbad692a25c6e6e72d11092d8f063f6ed1d048e) 86 | * 0.3.0 refactored exporter part, added: hair uvs, curves instancing, material global properties, fixed: object transformations when opened and saved in Studio 87 | * 0.2.4 added: automatic subdivision modifiers export to speed things up 88 | * 0.2.3 added: mxs export menu operator, quad export when using subdivision modifier, 2.75 compatibility 89 | * 0.2.2 added: mxs import (objects, emitters, cameras and sun selectively), save extension materials to mxm, embed particles in mxs (saving of external .bin files is now optional) 90 | * 0.2.1 added: extension materials creation and editing inside blender 91 | * 0.2.0 added: much faster large mesh export on Mac OS X, Extra Sampling panel, Volumetrics extension (constant and noise 3d), external particle bin works with animation export 92 | * 0.1.9 added: MXS References, Windows installation simplified 93 | * 0.1.8 added: Linux and Windows support, cloner extension, lots of refactoring 94 | * 0.1.7 added: presets, texture panel, basic material preview, RFBin export with size per particle, lots of refactoring, Linux and Windows ports are working without extensions 95 | * 0.1.6 added: maxwell hair, subdivision, scatter and particles export as realflow bin, fixed: render shortcuts, ui spaceing 96 | * 0.1.5 added: maxwell grass modifier, incremental export, minor ui tweaks and additions, launch multiple instances of maxwell and studio, fixed: material placeholders on triangle groups, maxwell particles transformation, error reporting 97 | * 0.1.4 added: render layers panel, fixed: path handling, instance bases on hidden layers and many more 98 | * 0.1.3 first release 99 | 100 | *** 101 | 102 | **links:** 103 | 104 | [blenderartist.org forum thread](http://blenderartists.org/forum/showthread.php?366067-Maxwell-Render-integration-for-Blender-%28different-one%29) 105 | 106 | [maxwellrender.com forum thread](http://www.maxwellrender.com/forum/viewtopic.php?f=138&t=43385) 107 | -------------------------------------------------------------------------------- /support/read_mxs_ref.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import json 29 | import shutil 30 | import argparse 31 | import textwrap 32 | import os 33 | import struct 34 | 35 | 36 | quiet = False 37 | LOG_FILE_PATH = None 38 | 39 | 40 | def log(msg, indent=0): 41 | if(quiet): 42 | return 43 | m = "{0}> {1}".format(" " * indent, msg) 44 | print(m) 45 | if(LOG_FILE_PATH is not None): 46 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 47 | f.write("{}{}".format(m, "\n")) 48 | 49 | 50 | class PercentDone(): 51 | def __init__(self, total, prefix="> ", indent=0): 52 | self.current = 0 53 | self.percent = -1 54 | self.last = -1 55 | self.total = total 56 | self.prefix = prefix 57 | self.indent = indent 58 | self.t = " " 59 | self.r = "\r" 60 | self.n = "\n" 61 | 62 | def step(self, numdone=1): 63 | if(quiet): 64 | return 65 | self.current += numdone 66 | self.percent = int(self.current / (self.total / 100)) 67 | if(self.percent > self.last): 68 | sys.stdout.write(self.r) 69 | sys.stdout.write("{0}{1}{2}%".format(self.t * self.indent, self.prefix, self.percent)) 70 | self.last = self.percent 71 | if(self.percent >= 100 or self.total == self.current): 72 | sys.stdout.write(self.r) 73 | sys.stdout.write("{0}{1}{2}%{3}".format(self.t * self.indent, self.prefix, 100, self.n)) 74 | if(LOG_FILE_PATH is not None): 75 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 76 | f.write("{}".format("{0}{1}{2}%{3}".format(self.t * self.indent, self.prefix, 100, self.n))) 77 | 78 | 79 | class MXSBinRefVertsWriter(): 80 | def __init__(self, path, data, ): 81 | o = "@" 82 | with open("{0}.tmp".format(path), 'wb') as f: 83 | p = struct.pack 84 | fw = f.write 85 | # header 86 | fw(p(o + "7s", 'BINREFV'.encode('utf-8'))) 87 | fw(p(o + "?", False)) 88 | # number of objects 89 | fw(p(o + "i", len(data))) 90 | for i in range(len(data)): 91 | d = data[i] 92 | name = d['name'] 93 | base = d['base'] 94 | pivot = d['pivot'] 95 | vertices = d['vertices'] 96 | # name 97 | fw(p(o + "250s", name.encode('utf-8'))) 98 | # base and pivot 99 | fw(p(o + "12d", *[a for b in base for a in b])) 100 | fw(p(o + "12d", *[a for b in pivot for a in b])) 101 | # number of vertices 102 | fw(p(o + "i", len(vertices) * 3)) 103 | # vertices 104 | lv = len(vertices) 105 | fw(p(o + "{}d".format(lv * 3), *[f for v in vertices for f in v])) 106 | fw(p(o + "?", False)) 107 | # swap files 108 | if(os.path.exists(path)): 109 | os.remove(path) 110 | shutil.move("{0}.tmp".format(path), path) 111 | self.path = path 112 | 113 | 114 | def get_objects_names(scene): 115 | it = CmaxwellObjectIterator() 116 | o = it.first(scene) 117 | l = [] 118 | while not o.isNull(): 119 | name, _ = o.getName() 120 | l.append(name) 121 | o = it.next() 122 | return l 123 | 124 | 125 | def object(o): 126 | is_instance, _ = o.isInstance() 127 | is_mesh, _ = o.isMesh() 128 | if(is_instance == 0 and is_mesh == 0): 129 | return None 130 | 131 | def get_verts(o): 132 | vs = [] 133 | nv, _ = o.getVerticesCount() 134 | for i in range(nv): 135 | v, _ = o.getVertex(i, 0) 136 | vs.append((v.x(), v.y(), v.z())) 137 | return vs 138 | 139 | b, p = global_transform(o) 140 | r = {'name': o.getName()[0], 141 | 'base': b, 142 | 'pivot': p, 143 | 'vertices': [], } 144 | if(is_instance == 1): 145 | io = o.getInstanced() 146 | r['vertices'] = get_verts(io) 147 | else: 148 | r['vertices'] = get_verts(o) 149 | return r 150 | 151 | 152 | def global_transform(o): 153 | cb, _ = o.getWorldTransform() 154 | o = cb.origin 155 | x = cb.xAxis 156 | y = cb.yAxis 157 | z = cb.zAxis 158 | rb = [[o.x(), o.y(), o.z()], [x.x(), x.y(), x.z()], [y.x(), y.y(), y.z()], [z.x(), z.y(), z.z()]] 159 | rp = ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), ) 160 | return rb, rp 161 | 162 | 163 | def main(args): 164 | log("maxwell meshes to data:", 1) 165 | # scene 166 | mp = args.mxs_path 167 | log("reading mxs scene from: {0}".format(mp), 2) 168 | scene = Cmaxwell(mwcallback) 169 | ok = scene.readMXS(mp) 170 | if(not ok): 171 | if(not os.path.exists(mp)): 172 | raise RuntimeError("Error during reading scene {}, file not found..".format(mp)) 173 | raise RuntimeError("Error during reading scene {}".format(mp)) 174 | # read meshes and instances 175 | nms = get_objects_names(scene) 176 | data = [] 177 | log("reading meshes..", 2) 178 | progress = PercentDone(len(nms), prefix="> ", indent=2, ) 179 | for n in nms: 180 | d = None 181 | o = scene.getObject(n) 182 | if(not o.isNull()): 183 | if(o.isMesh()[0] == 1 and o.isInstance()[0] == 0): 184 | # is mesh, read its vertices 185 | d = object(o) 186 | if(d is not None): 187 | data.append(d) 188 | progress.step() 189 | log("reading instances..", 2) 190 | progress = PercentDone(len(nms), prefix="> ", indent=2, ) 191 | for n in nms: 192 | d = None 193 | o = scene.getObject(n) 194 | if(not o.isNull()): 195 | if(o.isMesh()[0] == 0 and o.isInstance()[0] == 1): 196 | # is instance, find instanced mesh and just copy vertices 197 | io = o.getInstanced() 198 | ion = io.getName()[0] 199 | for a in data: 200 | if(a['name'] == ion): 201 | b, p = global_transform(o) 202 | d = {'name': o.getName()[0], 203 | 'base': b, 204 | 'pivot': p, 205 | 'vertices': a['vertices'][:], } 206 | if(d is not None): 207 | data.append(d) 208 | progress.step() 209 | # save data 210 | log("serializing..", 2) 211 | p = args.scene_data_path 212 | w = MXSBinRefVertsWriter(p, data) 213 | log("done.", 2) 214 | 215 | 216 | if __name__ == "__main__": 217 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Read vertices locations for MXS reference viewport diplay'''), 218 | epilog='', formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 219 | parser.add_argument('-q', '--quiet', action='store_true', help='no logging except errors') 220 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 221 | parser.add_argument('log_file', type=str, help='path to log file') 222 | parser.add_argument('mxs_path', type=str, help='path to source .mxs') 223 | parser.add_argument('scene_data_path', type=str, help='path to serialized data') 224 | args = parser.parse_args() 225 | 226 | PYMAXWELL_PATH = args.pymaxwell_path 227 | 228 | try: 229 | from pymaxwell import * 230 | except ImportError: 231 | if(not os.path.exists(PYMAXWELL_PATH)): 232 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 233 | sys.path.insert(0, PYMAXWELL_PATH) 234 | from pymaxwell import * 235 | 236 | quiet = args.quiet 237 | LOG_FILE_PATH = args.log_file 238 | 239 | try: 240 | # import cProfile, pstats, io 241 | # pr = cProfile.Profile() 242 | # pr.enable() 243 | 244 | main(args) 245 | 246 | # pr.disable() 247 | # s = io.StringIO() 248 | # sortby = 'cumulative' 249 | # ps = pstats.Stats(pr, stream=s).sort_stats(sortby) 250 | # ps.print_stats() 251 | # print(s.getvalue()) 252 | 253 | except Exception as e: 254 | import traceback 255 | m = traceback.format_exc() 256 | log(m) 257 | sys.exit(1) 258 | sys.exit(0) 259 | -------------------------------------------------------------------------------- /rfbin.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import os 20 | import re 21 | import struct 22 | import shutil 23 | import string 24 | import time 25 | import datetime 26 | import math 27 | 28 | import bpy 29 | from bpy.types import Operator 30 | from bpy_extras.io_utils import ExportHelper 31 | from bpy.props import StringProperty, FloatProperty, BoolProperty 32 | from mathutils import Matrix, Vector 33 | 34 | from .log import log, LogStyles 35 | 36 | 37 | class RFBinWriter(): 38 | """RealFlow particle .bin writer""" 39 | def __init__(self, directory, name, frame, particles, fps=24, size=0.001, log_indent=0, ): 40 | """ 41 | directory string (path) 42 | name string ascii 43 | frame int >= 0 44 | particles list of (id int, x float, y float, z float, normal x float, normal y float, normal z float, velocity x float, velocity y float, velocity z float, radius float) 45 | fps int > 0 46 | size float > 0 47 | """ 48 | cn = self.__class__.__name__ 49 | self.log_indent = log_indent 50 | log("{}:".format(cn), 0 + self.log_indent, LogStyles.MESSAGE) 51 | 52 | if(not os.path.exists(directory)): 53 | raise OSError("{}: did you point me to an imaginary directory? ({})".format(cn, directory)) 54 | if(not os.path.isdir(directory)): 55 | raise OSError("{}: not a directory. ({})".format(cn, directory)) 56 | if(not os.access(directory, os.W_OK)): 57 | raise OSError("{}: no write access. ({})".format(cn, directory)) 58 | self.directory = directory 59 | 60 | if(name == ""): 61 | raise ValueError("{}: name is an empty string".format(cn)) 62 | ch = "-_.() {0}{1}".format(string.ascii_letters, string.digits) 63 | valid = "".join(c for c in name if c in ch) 64 | if(name != valid): 65 | log("invalid name.. changed to {0}".format(valid), 1 + self.log_indent, LogStyles.WARNING) 66 | self.name = valid 67 | 68 | if(int(frame) < 0): 69 | raise ValueError("{}: frame is less than zero".format(cn)) 70 | self.frame = int(frame) 71 | 72 | self.extension = ".bin" 73 | self.path = os.path.join(self.directory, "{0}-{1}{2}".format(self.name, str(self.frame).zfill(5), self.extension)) 74 | 75 | particle_length = 11 + 3 76 | if(all(len(v) == particle_length for v in particles) is False): 77 | raise ValueError("{}: bad particle data.".format(cn)) 78 | self.particles = particles 79 | 80 | if(int(fps) < 0): 81 | raise ValueError("{}: fps is less than zero".format(cn)) 82 | self.fps = int(fps) 83 | 84 | if(size <= 0): 85 | raise ValueError("{}: size is less than/or zero".format(cn)) 86 | self.size = size 87 | 88 | self.version = 11 89 | 90 | self._write() 91 | 92 | def _write(self): 93 | self._t = time.time() 94 | p = self.path 95 | log("writing particles to: {0}".format(p), 1 + self.log_indent, ) 96 | with open("{0}.tmp".format(p), 'wb') as f: 97 | log("writing header..", 1 + self.log_indent, ) 98 | self._header(f) 99 | log("writing particles..", 1 + self.log_indent, ) 100 | self._particles(f) 101 | log("writing appendix..", 1 + self.log_indent, ) 102 | self._appendix(f) 103 | if(os.path.exists(p)): 104 | os.remove(p) 105 | shutil.move("{0}.tmp".format(p), p) 106 | log("done.", 1 + self.log_indent, ) 107 | _d = datetime.timedelta(seconds=time.time() - self._t) 108 | log("completed in {0}".format(_d), 1 + self.log_indent, ) 109 | 110 | def _header(self, f, ): 111 | p = struct.pack 112 | fw = f.write 113 | # magic 114 | fw(p("=i", 0xFABADA)) 115 | # name, should match with name 116 | fw(p("=250s", self.name.encode('utf-8'))) 117 | # version, scale, fluid type, simulation time 118 | fw(p("=hfif", self.version, 1.0, 9, 1.0)) 119 | # frame number 120 | fw(p("=i", self.frame)) 121 | # fps 122 | fw(p("=i", self.fps)) 123 | # number of particles 124 | fw(p("=i", len(self.particles))) 125 | # particle size 126 | fw(p("=f", self.size)) 127 | # pressure (max,min,average), speed (max,min,average), temperature (max,min,average) 128 | fw(p("=9f", 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)) 129 | # emitter_position (x,y,z), emitter_rotation (x,y,z), emitter_scale (x,y,z) 130 | fw(p("=9f", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0)) 131 | 132 | def _particles(self, f, ): 133 | p = struct.pack 134 | fw = f.write 135 | for v in self.particles: 136 | # 3 position 137 | fw(p("=3f", *v[1:4])) 138 | # 3 velocity 139 | fw(p("=3f", *v[7:10])) 140 | # 3 force, 3 vorticity 141 | fw(p("=ffffff", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)) 142 | # 3 normal 143 | fw(p("=3f", *v[4:7])) 144 | # neighbors 145 | fw(p("=i", 0, )) 146 | # 3 texture 147 | fw(p("=fff", *v[11:14])) 148 | # infobits, age, isolationtime, viscosity, density, pressure, mass, temperature 149 | fw(p("=hfffffff", 7, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0)) 150 | # id 151 | fw(p("=i", v[0])) 152 | 153 | def _appendix(self, f, ): 154 | p = struct.pack 155 | fw = f.write 156 | # number of additional data per particle 157 | fw(p("=i", 1)) 158 | # id of the data 159 | fw(p("=i", 2)) 160 | # type of the data 161 | fw(p("=i", 4)) 162 | # size of the data 163 | fw(p("=i", 4)) 164 | # owner of the particle id 165 | fw(p("=i", 0)) 166 | 167 | for v in self.particles: 168 | # additional data? 169 | fw(p("=?", True)) 170 | # additional data 171 | fw(p("=f", v[10])) 172 | 173 | # RF4 internal data 174 | fw(p("=?", False)) 175 | # RF5 internal data 176 | fw(p("=?", False)) 177 | 178 | 179 | class ExportRFBin(Operator, ExportHelper): 180 | bl_idname = "maxwell_render.export_bin" 181 | bl_label = 'Realflow Particles (.bin)' 182 | bl_description = 'Realflow Particles (.bin)' 183 | 184 | def _proper_bin_name_with_full_path(self, context, name, filepath): 185 | e = "bin" 186 | d, f = os.path.split(filepath) 187 | fnm, fext = os.path.splitext(f) 188 | if(fext != e): 189 | fnm = "{0}.{1}".format(fnm, fext) 190 | fext = "" 191 | if(fext == ""): 192 | fext = e 193 | if(name == ""): 194 | name = context.active_object.name 195 | cf = context.scene.frame_current 196 | binnm = "{0}-{1}.{2}".format(name, str(cf).zfill(5), e) 197 | path = os.path.join(d, binnm) 198 | return path 199 | 200 | def _filepath_update(self, context): 201 | d, n = os.path.split(self.filepath) 202 | p = re.compile('^.+-[0-9]{5}.bin$') 203 | if(not re.fullmatch(p, n)): 204 | n = os.path.splitext(os.path.split(self.filepath)[1])[0] 205 | self.filepath = ExportRFBin._proper_bin_name_with_full_path(self, context, n, self.filepath) 206 | 207 | # maxlen 1024 - len('-00000.bin'): '-00000.bin' filename frame number suffix and extension 208 | filepath = StringProperty(name="File Path", description="", maxlen=1024 - len('-00000.bin'), subtype='FILE_PATH', update=_filepath_update, ) 209 | filename_ext = ".bin" 210 | check_extension = True 211 | filter_glob = StringProperty(default="*.bin", options={'HIDDEN'}, ) 212 | 213 | use_velocity = BoolProperty(name="Particle Velocity", default=False, ) 214 | use_size = BoolProperty(name="Size Per Particle", default=False, ) 215 | size = FloatProperty(name="Size", default=0.1, min=0.000001, max=1000000.0, step=3, precision=6, ) 216 | use_uv = BoolProperty(name="Particle UV", default=False, ) 217 | uv_layer = StringProperty(name="UV Layer", default="", ) 218 | 219 | @classmethod 220 | def poll(cls, context): 221 | o = context.active_object 222 | if(o is not None): 223 | # there is active object 224 | if(len(o.particle_systems) > 0): 225 | # has particle systems 226 | if(o.particle_systems.active.settings.type == 'EMITTER'): 227 | # and is emitter (not hair) 228 | return True 229 | return False 230 | 231 | def invoke(self, context, event): 232 | p = context.blend_data.filepath 233 | d = os.path.split(p)[0] 234 | self.filepath = self._proper_bin_name_with_full_path(context, "", d) 235 | context.window_manager.fileselect_add(self) 236 | return {'RUNNING_MODAL'} 237 | 238 | def draw(self, context): 239 | l = self.layout 240 | 241 | c = l.column() 242 | c.prop(self, 'use_velocity') 243 | c.prop(self, 'use_size') 244 | c.prop(self, 'size') 245 | 246 | c = l.column() 247 | c.prop(self, 'use_uv') 248 | o = context.active_object 249 | r = c.row() 250 | r.prop_search(self, 'uv_layer', o.data, 'uv_textures', ) 251 | if(len(o.data.uv_textures) == 0): 252 | c.enabled = False 253 | if(not self.use_uv): 254 | r.enabled = False 255 | 256 | def execute(self, context): 257 | log('Export Realflow Particles (.bin)', 0, LogStyles.MESSAGE, ) 258 | log('use_velocity: {}, use_size: {}, size: {}, use_uv: {}, uv_layer: "{}"'.format(self.use_velocity, self.use_size, self.size, self.use_uv, self.uv_layer, ), 1, ) 259 | 260 | o = context.active_object 261 | ps = o.particle_systems.active 262 | pset = ps.settings 263 | 264 | # no particles (number of particles set to zero) and no alive particles > kill export 265 | if(len(ps.particles) == 0): 266 | log("particle system {} has no particles".format(ps.name), 1, LogStyles.ERROR, ) 267 | self.report({'ERROR'}, "particle system {} has no particles".format(ps.name), ) 268 | return {'CANCELLED'} 269 | ok = False 270 | for p in ps.particles: 271 | if(p.alive_state == "ALIVE"): 272 | ok = True 273 | break 274 | if(not ok): 275 | log("particle system {} has no 'ALIVE' particles".format(ps.name), 1, LogStyles.ERROR, ) 276 | self.report({'ERROR'}, "particle system {} has no 'ALIVE' particles".format(ps.name), ) 277 | return {'CANCELLED'} 278 | 279 | mat = o.matrix_world.copy() 280 | mat.invert() 281 | 282 | locs = [] 283 | vels = [] 284 | sizes = [] 285 | 286 | # location, velocity and size from alive particles 287 | for part in ps.particles: 288 | if(part.alive_state == "ALIVE"): 289 | l = part.location.copy() 290 | l = mat * l 291 | locs.append(l) 292 | if(self.use_velocity): 293 | v = part.velocity.copy() 294 | v = mat * v 295 | vels.append(v) 296 | else: 297 | vels.append(Vector((0.0, 0.0, 0.0))) 298 | # size per particle 299 | if(self.use_size): 300 | sizes.append(part.size / 2) 301 | else: 302 | sizes.append(self.size / 2) 303 | 304 | # transform 305 | # TODO: axis conversion is overly complicated, is it? 306 | ROTATE_X_90 = Matrix.Rotation(math.radians(90.0), 4, 'X') 307 | rfms = Matrix.Scale(1.0, 4) 308 | rfms[0][0] = -1.0 309 | rfmr = Matrix.Rotation(math.radians(-90.0), 4, 'Z') 310 | rfm = rfms * rfmr * ROTATE_X_90 311 | mry90 = Matrix.Rotation(math.radians(90.0), 4, 'Y') 312 | for i, l in enumerate(locs): 313 | locs[i] = Vector(l * rfm).to_tuple() 314 | if(self.use_velocity): 315 | for i, v in enumerate(vels): 316 | vels[i] = Vector(v * rfm).to_tuple() 317 | 318 | # particle uvs 319 | if(self.uv_layer is not "" and self.use_uv): 320 | uv_no = 0 321 | for i, uv in enumerate(o.data.uv_textures): 322 | if(self.uv_layer == uv.name): 323 | uv_no = i 324 | break 325 | 326 | uv_locs = tuple() 327 | 328 | if(len(ps.child_particles) > 0): 329 | # NOT TO DO: use bvhtree to make uvs for particles, like with hair - no way to get child particles locations = no uvs 330 | log("child particles uvs are not supported yet..", 1, LogStyles.WARNING, ) 331 | self.report({'WARNING'}, "child particles uvs are not supported yet..") 332 | else: 333 | # no child particles, use 'uv_on_emitter' 334 | nc0 = len(ps.particles) 335 | nc1 = len(ps.child_particles) - nc0 336 | uv_no = 0 337 | for i, uv in enumerate(o.data.uv_textures): 338 | if(self.uv_layer == uv.name): 339 | uv_no = i 340 | break 341 | mod = None 342 | for m in o.modifiers: 343 | if(m.type == 'PARTICLE_SYSTEM'): 344 | if(m.particle_system == ps): 345 | mod = m 346 | break 347 | uv_locs = tuple() 348 | for i, p in enumerate(ps.particles): 349 | co = ps.uv_on_emitter(mod, p, particle_no=i, uv_no=uv_no, ) 350 | # (x, y, 0.0, ) 351 | t = co.to_tuple() + (0.0, ) 352 | uv_locs += (t[0], 1.0 - t[1], t[2], ) 353 | if(nc1 != 0): 354 | ex = int(nc1 / nc0) 355 | for i in range(ex): 356 | uv_locs += uv_locs 357 | has_uvs = True 358 | else: 359 | uv_locs = [0.0] * (len(ps.particles) * 3) 360 | if(self.use_uv): 361 | log("emitter has no UVs or no UV is selected to be used.. UVs will be exported, but set to (0.0, 0.0)", 1, LogStyles.WARNING, ) 362 | self.report({'WARNING'}, "emitter has no UVs or no UV is selected to be used.. UVs will be exported, but set to (0.0, 0.0)") 363 | 364 | flip_xy = Matrix(((-1.0, 0.0, 0.0, 0.0), (0.0, -1.0, 0.0, 0.0), (0.0, 0.0, 1.0, 0.0), (0.0, 0.0, 0.0, 1.0))) 365 | fv = Vector((-1.0, -1.0, 0.0)) 366 | particles = [] 367 | for i, ploc in enumerate(locs): 368 | # normal from velocity 369 | pnor = Vector(vels[i]) 370 | pnor.normalize() 371 | uv = uv_locs[i * 3:(i * 3) + 3] 372 | uvv = Vector(uv).reflect(fv) * flip_xy 373 | uvt = (uvv.z, uvv.x, uvv.y, ) 374 | particles.append((i, ) + tuple(ploc[:3]) + pnor.to_tuple() + tuple(vels[i][:3]) + (sizes[i], ) + uvt, ) 375 | 376 | # and now.. export! 377 | h, t = os.path.split(self.filepath) 378 | n, e = os.path.splitext(t) 379 | # remove frame number automaticaly added in ui 380 | n = n[:-6] 381 | 382 | cf = bpy.context.scene.frame_current 383 | prms = {'directory': bpy.path.abspath(h), 384 | 'name': "{}".format(n), 385 | 'frame': cf, 386 | 'particles': particles, 387 | 'fps': bpy.context.scene.render.fps, 388 | # blender's size is in fact diameter, but we need radius.. 389 | 'size': 1.0 if self.use_size else self.size / 2, 390 | 'log_indent': 1, } 391 | rfbw = RFBinWriter(**prms) 392 | 393 | log('done.', 1, ) 394 | 395 | return {'FINISHED'} 396 | 397 | def _menu(self, context): 398 | self.layout.operator(ExportRFBin.bl_idname, text="Realflow Particles (.bin)") 399 | 400 | @classmethod 401 | def register(cls): 402 | bpy.types.INFO_MT_file_export.append(cls._menu) 403 | 404 | @classmethod 405 | def unregister(cls): 406 | bpy.types.INFO_MT_file_export.remove(cls._menu) 407 | -------------------------------------------------------------------------------- /support/read_mxs.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import json 29 | import shutil 30 | import argparse 31 | import textwrap 32 | import os 33 | 34 | 35 | quiet = False 36 | LOG_FILE_PATH = None 37 | 38 | 39 | def log(msg, indent=0): 40 | if(quiet): 41 | return 42 | m = "{0}> {1}".format(" " * indent, msg) 43 | print(m) 44 | if(LOG_FILE_PATH is not None): 45 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 46 | f.write("{}{}".format(m, "\n")) 47 | 48 | 49 | class PercentDone(): 50 | def __init__(self, total, prefix="> ", indent=0): 51 | self.current = 0 52 | self.percent = -1 53 | self.last = -1 54 | self.total = total 55 | self.prefix = prefix 56 | self.indent = indent 57 | self.t = " " 58 | self.r = "\r" 59 | self.n = "\n" 60 | 61 | def step(self, numdone=1): 62 | if(quiet): 63 | return 64 | self.current += numdone 65 | self.percent = int(self.current / (self.total / 100)) 66 | if(self.percent > self.last): 67 | sys.stdout.write(self.r) 68 | sys.stdout.write("{0}{1}{2}%".format(self.t * self.indent, self.prefix, self.percent)) 69 | self.last = self.percent 70 | if(self.percent >= 100 or self.total == self.current): 71 | sys.stdout.write(self.r) 72 | sys.stdout.write("{0}{1}{2}%{3}".format(self.t * self.indent, self.prefix, 100, self.n)) 73 | if(LOG_FILE_PATH is not None): 74 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 75 | f.write("{}".format("{0}{1}{2}%{3}".format(self.t * self.indent, self.prefix, 100, self.n))) 76 | 77 | 78 | def get_objects_names(scene): 79 | it = CmaxwellObjectIterator() 80 | o = it.first(scene) 81 | l = [] 82 | while not o.isNull(): 83 | name, _ = o.getName() 84 | l.append(name) 85 | o = it.next() 86 | return l 87 | 88 | 89 | def base_and_pivot(obj): 90 | b, p, _ = obj.getBaseAndPivot() 91 | o = b.origin 92 | x = b.xAxis 93 | y = b.yAxis 94 | z = b.zAxis 95 | rb = [[o.x(), o.y(), o.z()], [x.x(), x.y(), x.z()], [y.x(), y.y(), y.z()], [z.x(), z.y(), z.z()]] 96 | 97 | o = p.origin 98 | x = p.xAxis 99 | y = p.yAxis 100 | z = p.zAxis 101 | rp = [[o.x(), o.y(), o.z()], [x.x(), x.y(), x.z()], [y.x(), y.y(), y.z()], [z.x(), z.y(), z.z()]] 102 | 103 | is_init, _ = obj.isPosRotScaleInitialized() 104 | if(is_init): 105 | l, _ = obj.getPosition() 106 | rl = (l.x(), l.y(), l.z()) 107 | r, _ = obj.getRotation() 108 | rr = (r.x(), r.y(), r.z()) 109 | s, _ = obj.getScale() 110 | rs = (s.x(), s.y(), s.z()) 111 | else: 112 | rl = None 113 | rr = None 114 | rs = None 115 | 116 | return rb, rp, rl, rr, rs 117 | 118 | 119 | def uncorrect_focal_length(step): 120 | flc = step[3] 121 | o = step[0] 122 | fp = step[1] 123 | d = Cvector() 124 | d.substract(fp, o) 125 | fd = d.norm() 126 | fluc = 1.0 / (1.0 / flc - 1 / fd) 127 | return fluc 128 | 129 | 130 | def camera(c): 131 | v = c.getValues() 132 | v = {'name': v[0], 133 | 'nSteps': v[1], 134 | 'shutter': v[2], 135 | 'filmWidth': v[3], 136 | 'filmHeight': v[4], 137 | 'iso': v[5], 138 | 'pDiaphragmType': v[6], 139 | 'angle': v[7], 140 | 'nBlades': v[8], 141 | 'fps': v[9], 142 | 'xRes': v[10], 143 | 'yRes': v[11], 144 | 'pixelAspect': v[12], 145 | 'lensType': v[13], } 146 | s = c.getStep(0) 147 | o = s[0] 148 | f = s[1] 149 | u = s[2] 150 | 151 | # skip weird cameras 152 | flc = s[3] 153 | co = s[0] 154 | fp = s[1] 155 | d = Cvector() 156 | d.substract(fp, co) 157 | fd = d.norm() 158 | if(flc == 0.0 or fd == 0.0): 159 | log("WARNING: {}: impossible camera, skipping..".format(v['name']), 2) 160 | return None 161 | 162 | r = {'name': v['name'], 163 | 'shutter': 1.0 / v['shutter'], 164 | 'iso': v['iso'], 165 | 'x_res': v['xRes'], 166 | 'y_res': v['yRes'], 167 | 'pixel_aspect': v['pixelAspect'], 168 | 'origin': (o.x(), o.y(), o.z()), 169 | 'focal_point': (f.x(), f.y(), f.z()), 170 | 'up': (u.x(), u.y(), u.z()), 171 | 'focal_length': uncorrect_focal_length(s) * 1000.0, 172 | 'f_stop': s[4], 173 | 'film_width': round(v['filmWidth'] * 1000.0, 3), 174 | 'film_height': round(v['filmHeight'] * 1000.0, 3), 175 | 'active': False, 176 | 'sensor_fit': None, 177 | 'shift_x': 0.0, 178 | 'shift_y': 0.0, 179 | 'zclip': False, 180 | 'zclip_near': 0.0, 181 | 'zclip_far': 1000000.0, 182 | 'type': 'CAMERA', } 183 | if(r['film_width'] > r['film_height']): 184 | r['sensor_fit'] = 'HORIZONTAL' 185 | else: 186 | r['sensor_fit'] = 'VERTICAL' 187 | cp = c.getCutPlanes() 188 | if(cp[2] is True): 189 | r['zclip'] = True 190 | r['zclip_near'] = cp[0] 191 | r['zclip_far'] = cp[1] 192 | sl = c.getShiftLens() 193 | r['shift_x'] = sl[0] 194 | r['shift_y'] = sl[1] 195 | d = c.getDiaphragm() 196 | r['diaphragm_type'] = d[0][0] 197 | r['diaphragm_angle'] = d[1] 198 | r['diaphragm_blades'] = d[2] 199 | return r 200 | 201 | 202 | def object(o): 203 | object_name, _ = o.getName() 204 | is_instance, _ = o.isInstance() 205 | is_mesh, _ = o.isMesh() 206 | if(is_instance == 0 and is_mesh == 0): 207 | log("WARNING: {}: only empties, meshes and instances are supported..".format(object_name), 2) 208 | return None 209 | 210 | # skip not posrotscale initialized objects 211 | is_init, _ = o.isPosRotScaleInitialized() 212 | if(not is_init): 213 | # log("WARNING: {}: object is not initialized, skipping..".format(object_name), 2) 214 | log("WARNING: {}: object is not initialized..".format(object_name), 2) 215 | # return None 216 | 217 | r = {'name': o.getName()[0], 218 | 'vertices': [], 219 | 'normals': [], 220 | 'triangles': [], 221 | 'trianglesUVW': [], 222 | 'matrix': (), 223 | 'parent': None, 224 | 'type': '', 225 | 'materials': [], 226 | 'nmats': 0, 227 | 'matnames': [], } 228 | if(is_instance == 1): 229 | io = o.getInstanced() 230 | ion = io.getName()[0] 231 | b, p, loc, rot, sca = base_and_pivot(o) 232 | r = {'name': o.getName()[0], 233 | 'base': b, 234 | 'pivot': p, 235 | 'location': loc, 236 | 'rotation': rot, 237 | 'scale': sca, 238 | 'parent': None, 239 | 'type': 'INSTANCE', 240 | 'instanced': ion, } 241 | # no multi material instances, always one material per instance 242 | m, _ = o.getMaterial() 243 | if(m.isNull() == 1): 244 | r['material'] = None 245 | else: 246 | r['material'] = o.getName() 247 | p, _ = o.getParent() 248 | if(not p.isNull()): 249 | r['parent'] = p.getName()[0] 250 | 251 | cid, _ = o.getColorID() 252 | rgb8 = cid.toRGB8() 253 | col = [str(rgb8.r()), str(rgb8.g()), str(rgb8.b())] 254 | r['colorid'] = ", ".join(col) 255 | 256 | h = [] 257 | if(o.getHideToCamera()): 258 | h.append("C") 259 | if(o.getHideToGI()): 260 | h.append("GI") 261 | if(o.getHideToReflectionsRefractions()): 262 | h.append("RR") 263 | r['hidden'] = ", ".join(h) 264 | 265 | r['referenced_mxs'] = False 266 | r['referenced_mxs_path'] = None 267 | rmp = io.getReferencedScenePath() 268 | if(rmp != ""): 269 | r['referenced_mxs'] = True 270 | r['referenced_mxs_path'] = rmp 271 | 272 | return r 273 | # counts 274 | nv, _ = o.getVerticesCount() 275 | nn, _ = o.getNormalsCount() 276 | nt, _ = o.getTrianglesCount() 277 | nppv, _ = o.getPositionsPerVertexCount() 278 | ppv = 0 279 | 280 | r['referenced_mxs'] = False 281 | r['referenced_mxs_path'] = None 282 | 283 | if(nv > 0): 284 | r['type'] = 'MESH' 285 | 286 | cid, _ = o.getColorID() 287 | rgb8 = cid.toRGB8() 288 | col = [str(rgb8.r()), str(rgb8.g()), str(rgb8.b())] 289 | r['colorid'] = ", ".join(col) 290 | 291 | h = [] 292 | if(o.getHideToCamera()): 293 | h.append("C") 294 | if(o.getHideToGI()): 295 | h.append("GI") 296 | if(o.getHideToReflectionsRefractions()): 297 | h.append("RR") 298 | r['hidden'] = ", ".join(h) 299 | else: 300 | r['type'] = 'EMPTY' 301 | 302 | rmp = o.getReferencedScenePath() 303 | if(rmp != ""): 304 | r['referenced_mxs'] = True 305 | r['referenced_mxs_path'] = rmp 306 | 307 | cid, _ = o.getColorID() 308 | rgb8 = cid.toRGB8() 309 | col = [str(rgb8.r()), str(rgb8.g()), str(rgb8.b())] 310 | r['colorid'] = ", ".join(col) 311 | 312 | if(nppv - 1 != ppv and nv != 0): 313 | log("WARNING: only one position per vertex is supported..", 2) 314 | # vertices 315 | for i in range(nv): 316 | v, _ = o.getVertex(i, ppv) 317 | # (float x, float y, float z) 318 | r['vertices'].append((v.x(), v.y(), v.z())) 319 | # normals 320 | for i in range(nn): 321 | v, _ = o.getNormal(i, ppv) 322 | # (float x, float y, float z) 323 | r['normals'].append((v.x(), v.y(), v.z())) 324 | # triangles 325 | for i in range(nt): 326 | t = o.getTriangle(i) 327 | # (int v1, int v2, int v3, int n1, int n2, int n3) 328 | r['triangles'].append(t) 329 | # materials 330 | mats = [] 331 | for i in range(nt): 332 | m, _ = o.getTriangleMaterial(i) 333 | if(m.isNull() == 1): 334 | n = None 335 | else: 336 | n = m.getName() 337 | if(n not in mats): 338 | mats.append(n) 339 | r['materials'].append((i, n)) 340 | r['nmats'] = len(mats) 341 | r['matnames'] = mats 342 | # uv channels 343 | ncuv, _ = o.getChannelsUVWCount() 344 | for cuv in range(ncuv): 345 | # uv triangles 346 | r['trianglesUVW'].append([]) 347 | for i in range(nt): 348 | t = o.getTriangleUVW(i, cuv) 349 | # float u1, float v1, float w1, float u2, float v2, float w2, float u3, float v3, float w3 350 | r['trianglesUVW'][cuv].append(t) 351 | # base and pivot to matrix 352 | b, p, loc, rot, sca = base_and_pivot(o) 353 | r['base'] = b 354 | r['pivot'] = p 355 | r['location'] = loc 356 | r['rotation'] = rot 357 | r['scale'] = sca 358 | # parent 359 | p, _ = o.getParent() 360 | if(not p.isNull()): 361 | r['parent'] = p.getName()[0] 362 | return r 363 | 364 | 365 | def is_emitter(o): 366 | is_instance, _ = o.isInstance() 367 | is_mesh, _ = o.isMesh() 368 | if(not is_mesh and not is_instance): 369 | return False 370 | if(is_mesh): 371 | nt, _ = o.getTrianglesCount() 372 | mats = [] 373 | for i in range(nt): 374 | m, _ = o.getTriangleMaterial(i) 375 | if(not m.isNull()): 376 | if(m not in mats): 377 | mats.append(m) 378 | for m in mats: 379 | nl, _ = m.getNumLayers() 380 | for i in range(nl): 381 | l = m.getLayer(i) 382 | e = l.getEmitter() 383 | if(not e.isNull()): 384 | return True 385 | if(is_instance): 386 | m, _ = o.getMaterial() 387 | if(not m.isNull()): 388 | nl, _ = m.getNumLayers() 389 | for i in range(nl): 390 | l = m.getLayer(i) 391 | e = l.getEmitter() 392 | if(not e.isNull()): 393 | return True 394 | return False 395 | 396 | 397 | def global_transform(o): 398 | cb, _ = o.getWorldTransform() 399 | o = cb.origin 400 | x = cb.xAxis 401 | y = cb.yAxis 402 | z = cb.zAxis 403 | rb = [[o.x(), o.y(), o.z()], [x.x(), x.y(), x.z()], [y.x(), y.y(), y.z()], [z.x(), z.y(), z.z()]] 404 | rp = ((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), ) 405 | return rb, rp 406 | 407 | 408 | def main(args): 409 | log("maxwell meshes to data:", 1) 410 | # scene 411 | mp = args.mxs_path 412 | log("reading mxs scene from: {0}".format(mp), 2) 413 | scene = Cmaxwell(mwcallback) 414 | ok = scene.readMXS(mp) 415 | if(not ok): 416 | if(not os.path.exists(mp)): 417 | raise RuntimeError("Error during reading scene {}, file not found..".format(mp)) 418 | raise RuntimeError("Error during reading scene {}".format(mp)) 419 | if(scene.isProtectionEnabled()): 420 | raise RuntimeError("Protected MXS ({})".format(mp)) 421 | # objects 422 | nms = get_objects_names(scene) 423 | data = [] 424 | if(args.emitters and not args.objects): 425 | # only emitter objects 426 | log("converting emitters..", 2) 427 | for n in nms: 428 | d = None 429 | o = scene.getObject(n) 430 | if(is_emitter(o)): 431 | d = object(o) 432 | if(d is not None): 433 | b, p = global_transform(o) 434 | d['base'] = b 435 | d['pivot'] = p 436 | d['parent'] = None 437 | data.append(d) 438 | elif(args.objects): 439 | # objects to data 440 | log("converting empties, objects and instances..", 2) 441 | progress = PercentDone(len(nms), prefix="> ", indent=2, ) 442 | for n in nms: 443 | d = None 444 | o = scene.getObject(n) 445 | d = object(o) 446 | if(d is not None): 447 | data.append(d) 448 | progress.step() 449 | if(args.cameras): 450 | # cameras to data 451 | log("converting cameras..", 2) 452 | nms = scene.getCameraNames() 453 | cams = [] 454 | if(type(nms) == list): 455 | for n in nms: 456 | cams.append(scene.getCamera(n)) 457 | for c in cams: 458 | d = camera(c) 459 | if(d is not None): 460 | data.append(d) 461 | # set active camera 462 | if(len(cams) > 1): 463 | # if there is just one camera, this behaves badly. 464 | # use it just when there are two or more cameras.. 465 | active_cam = scene.getActiveCamera() 466 | active_cam_name = active_cam.getName() 467 | for o in data: 468 | if(o['type'] == 'CAMERA'): 469 | if(o['name'] == active_cam_name): 470 | o['active'] = True 471 | else: 472 | for o in data: 473 | if(o['type'] == 'CAMERA'): 474 | o['active'] = True 475 | if(args.sun): 476 | # sun 477 | env = scene.getEnvironment() 478 | if(env.getSunProperties()[0] == 1): 479 | log("converting sun..", 2) 480 | if(env.getSunPositionType() == 2): 481 | v, _ = env.getSunDirection() 482 | else: 483 | v, _ = env.getSunDirectionUsedForRendering() 484 | d = {'name': "The Sun", 485 | 'xyz': (v.x(), v.y(), v.z()), 486 | 'type': 'SUN', } 487 | data.append(d) 488 | # save data 489 | log("serializing..", 2) 490 | p = args.scene_data_path 491 | with open("{0}.tmp".format(p), 'w', encoding='utf-8', ) as f: 492 | json.dump(data, f, skipkeys=False, ensure_ascii=False, indent=4, ) 493 | if(os.path.exists(p)): 494 | os.remove(p) 495 | shutil.move("{0}.tmp".format(p), p) 496 | log("done.", 2) 497 | 498 | 499 | if __name__ == "__main__": 500 | parser = argparse.ArgumentParser(description=textwrap.dedent('''Make serialized data from Maxwell meshes and cameras'''), 501 | epilog='', formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 502 | parser.add_argument('-q', '--quiet', action='store_true', help='no logging except errors') 503 | parser.add_argument('-e', '--emitters', action='store_true', help='read emitters') 504 | parser.add_argument('-o', '--objects', action='store_true', help='read objects') 505 | parser.add_argument('-c', '--cameras', action='store_true', help='read cameras') 506 | parser.add_argument('-s', '--sun', action='store_true', help='read sun') 507 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 508 | parser.add_argument('log_file', type=str, help='path to log file') 509 | parser.add_argument('mxs_path', type=str, help='path to source .mxs') 510 | parser.add_argument('scene_data_path', type=str, help='path to serialized data') 511 | args = parser.parse_args() 512 | 513 | PYMAXWELL_PATH = args.pymaxwell_path 514 | 515 | try: 516 | from pymaxwell import * 517 | except ImportError: 518 | if(not os.path.exists(PYMAXWELL_PATH)): 519 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 520 | sys.path.insert(0, PYMAXWELL_PATH) 521 | from pymaxwell import * 522 | 523 | quiet = args.quiet 524 | LOG_FILE_PATH = args.log_file 525 | 526 | try: 527 | # import cProfile, pstats, io 528 | # pr = cProfile.Profile() 529 | # pr.enable() 530 | 531 | main(args) 532 | 533 | # pr.disable() 534 | # s = io.StringIO() 535 | # sortby = 'cumulative' 536 | # ps = pstats.Stats(pr, stream=s).sort_stats(sortby) 537 | # ps.print_stats() 538 | # print(s.getvalue()) 539 | 540 | except Exception as e: 541 | import traceback 542 | m = traceback.format_exc() 543 | log(m) 544 | sys.exit(1) 545 | sys.exit(0) 546 | -------------------------------------------------------------------------------- /tmpio.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import os 20 | import shutil 21 | import struct 22 | import sys 23 | 24 | 25 | class MXSBinMeshWriter(): 26 | def __init__(self, path, name, num_positions, vertices, normals, triangles, triangle_normals, uv_channels, num_materials, triangle_materials, ): 27 | """ 28 | name sting 29 | num_positions int 30 | vertices [[(float x, float y, float z), ..., ], [...], ] 31 | normals [[(float x, float y, float z), ..., ], [...], ] 32 | triangles [(int iv0, int iv1, int iv2, int in0, int in1, int in2, ), ..., ], ] # (3x vertex index, 3x normal index) 33 | triangle_normals [[(float x, float y, float z), ..., ], [...], ] 34 | uv_channels [[(float u1, float v1, float w1, float u2, float v2, float w2, float u3, float v3, float w3, ), ..., ], ..., ] or None # ordered by uv index and ordered by triangle index 35 | num_materials int 36 | triangle_materials [(int tri_id, int mat_id), ..., ] or None 37 | """ 38 | o = "@" 39 | with open("{0}.tmp".format(path), 'wb') as f: 40 | p = struct.pack 41 | fw = f.write 42 | # header 43 | fw(p(o + "7s", 'BINMESH'.encode('utf-8'))) 44 | fw(p(o + "?", False)) 45 | # name 250 max length 46 | fw(p(o + "250s", name.encode('utf-8'))) 47 | # number of steps 48 | fw(p(o + "i", num_positions)) 49 | # number of vertices 50 | lv = len(vertices[0]) 51 | fw(p(o + "i", lv)) 52 | # vertex positions 53 | for i in range(num_positions): 54 | fw(p(o + "{}d".format(lv * 3), *[f for v in vertices[i] for f in v])) 55 | # vertex normals 56 | for i in range(num_positions): 57 | fw(p(o + "{}d".format(lv * 3), *[f for v in normals[i] for f in v])) 58 | # number triangle normals 59 | ltn = len(triangle_normals[0]) 60 | fw(p(o + "i", ltn)) 61 | # triangle normals 62 | for i in range(num_positions): 63 | fw(p(o + "{}d".format(ltn * 3), *[f for v in triangle_normals[i] for f in v])) 64 | # number of triangles 65 | lt = len(triangles) 66 | fw(p(o + "i", lt)) 67 | # triangles 68 | fw(p(o + "{}i".format(lt * 6), *[f for v in triangles for f in v])) 69 | # number of uv channels 70 | luc = len(uv_channels) 71 | fw(p(o + "i", luc)) 72 | # uv channels 73 | for i in range(luc): 74 | fw(p(o + "{}d".format(lt * 9), *[f for v in uv_channels[i] for f in v])) 75 | # number of materials 76 | fw(p(o + "i", num_materials)) 77 | # triangle materials 78 | fw(p(o + "{}i".format(lt * 2), *[f for v in triangle_materials for f in v])) 79 | # end 80 | fw(p(o + "?", False)) 81 | # swap files 82 | if(os.path.exists(path)): 83 | os.remove(path) 84 | shutil.move("{0}.tmp".format(path), path) 85 | self.path = path 86 | 87 | 88 | class MXSBinMeshReader(): 89 | def __init__(self, path): 90 | def r(f, b, o): 91 | d = struct.unpack_from(f, b, o) 92 | o += struct.calcsize(f) 93 | return d, o 94 | 95 | def r0(f, b, o): 96 | d = struct.unpack_from(f, b, o)[0] 97 | o += struct.calcsize(f) 98 | return d, o 99 | 100 | offset = 0 101 | with open(path, "rb") as bf: 102 | buff = bf.read() 103 | # endianness? 104 | signature = 20357755437992258 105 | l, _ = r0("q", buff, 0) 107 | if(l == signature): 108 | if(sys.byteorder != "little"): 109 | raise RuntimeError() 110 | order = "<" 111 | elif(b == signature): 112 | if(sys.byteorder != "big"): 113 | raise RuntimeError() 114 | order = ">" 115 | else: 116 | raise AssertionError("{}: not a MXSBinMesh file".format(self.__class__.__name__)) 117 | o = order 118 | # magic 119 | magic, offset = r0(o + "7s", buff, offset) 120 | magic = magic.decode(encoding="utf-8") 121 | if(magic != 'BINMESH'): 122 | raise RuntimeError() 123 | # throwaway 124 | _, offset = r(o + "?", buff, offset) 125 | # name 126 | name, offset = r0(o + "250s", buff, offset) 127 | name = name.decode(encoding="utf-8").replace('\x00', '') 128 | # number of steps 129 | num_positions, offset = r0(o + "i", buff, offset) 130 | # number of vertices 131 | lv, offset = r0(o + "i", buff, offset) 132 | # vertex positions 133 | vertices = [] 134 | for i in range(num_positions): 135 | vs, offset = r(o + "{}d".format(lv * 3), buff, offset) 136 | vs3 = [vs[i:i + 3] for i in range(0, len(vs), 3)] 137 | vertices.append(vs3) 138 | # vertex normals 139 | normals = [] 140 | for i in range(num_positions): 141 | ns, offset = r(o + "{}d".format(lv * 3), buff, offset) 142 | ns3 = [ns[i:i + 3] for i in range(0, len(ns), 3)] 143 | normals.append(ns3) 144 | # number of triangle normals 145 | ltn, offset = r0(o + "i", buff, offset) 146 | # triangle normals 147 | triangle_normals = [] 148 | for i in range(num_positions): 149 | tns, offset = r(o + "{}d".format(ltn * 3), buff, offset) 150 | tns3 = [tns[i:i + 3] for i in range(0, len(tns), 3)] 151 | triangle_normals.append(tns3) 152 | # number of triangles 153 | lt, offset = r0(o + "i", buff, offset) 154 | # triangles 155 | ts, offset = r(o + "{}i".format(lt * 6), buff, offset) 156 | triangles = [ts[i:i + 6] for i in range(0, len(ts), 6)] 157 | # number uv channels 158 | num_channels, offset = r0(o + "i", buff, offset) 159 | # uv channels 160 | uv_channels = [] 161 | for i in range(num_channels): 162 | uvc, offset = r(o + "{}d".format(lt * 9), buff, offset) 163 | uv9 = [uvc[i:i + 9] for i in range(0, len(uvc), 9)] 164 | uv_channels.append(uv9) 165 | # number of materials 166 | num_materials, offset = r0(o + "i", buff, offset) 167 | # triangle materials 168 | tms, offset = r(o + "{}i".format(2 * lt), buff, offset) 169 | triangle_materials = [tms[i:i + 2] for i in range(0, len(tms), 2)] 170 | # throwaway 171 | _, offset = r(o + "?", buff, offset) 172 | # and now.. eof 173 | if(offset != len(buff)): 174 | raise RuntimeError("expected EOF") 175 | # collect data 176 | self.data = {'name': name, 177 | 'num_positions': num_positions, 178 | 'vertices': vertices, 179 | 'normals': normals, 180 | 'triangles': triangles, 181 | 'triangle_normals': triangle_normals, 182 | 'uv_channels': uv_channels, 183 | 'num_materials': num_materials, 184 | 'triangle_materials': triangle_materials, } 185 | 186 | 187 | class MXSBinHairWriter(): 188 | def __init__(self, path, data): 189 | d = data 190 | o = "@" 191 | with open("{0}.tmp".format(path), 'wb') as f: 192 | p = struct.pack 193 | fw = f.write 194 | # header 195 | fw(p(o + "7s", 'BINHAIR'.encode('utf-8'))) 196 | fw(p(o + "?", False)) 197 | # number of floats 198 | n = len(d) 199 | fw(p(o + "i", n)) 200 | # floats 201 | fw(p(o + "{}d".format(n), *d)) 202 | # end 203 | fw(p(o + "?", False)) 204 | if(os.path.exists(path)): 205 | os.remove(path) 206 | shutil.move("{0}.tmp".format(path), path) 207 | self.path = path 208 | 209 | 210 | class MXSBinHairReader(): 211 | def __init__(self, path): 212 | self.offset = 0 213 | with open(path, "rb") as bf: 214 | self.bindata = bf.read() 215 | 216 | def r(f): 217 | d = struct.unpack_from(f, self.bindata, self.offset) 218 | self.offset += struct.calcsize(f) 219 | return d 220 | 221 | # endianness? 222 | signature = 23161492825065794 223 | l = r("q")[0] 226 | self.offset = 0 227 | if(l == signature): 228 | if(sys.byteorder != "little"): 229 | raise RuntimeError() 230 | self.order = "<" 231 | elif(b == signature): 232 | if(sys.byteorder != "big"): 233 | raise RuntimeError() 234 | self.order = ">" 235 | else: 236 | raise AssertionError("{}: not a MXSBinHair file".format(self.__class__.__name__)) 237 | o = self.order 238 | # magic 239 | self.magic = r(o + "7s")[0].decode(encoding="utf-8") 240 | if(self.magic != 'BINHAIR'): 241 | raise RuntimeError() 242 | _ = r(o + "?") 243 | # number floats 244 | self.num = r(o + "i")[0] 245 | self.data = r(o + "{}d".format(self.num)) 246 | e = r(o + "?") 247 | if(self.offset != len(self.bindata)): 248 | raise RuntimeError("expected EOF") 249 | 250 | 251 | class MXSBinParticlesWriter(): 252 | def __init__(self, path, data): 253 | d = data 254 | o = "@" 255 | with open("{0}.tmp".format(path), 'wb') as f: 256 | p = struct.pack 257 | fw = f.write 258 | # header 259 | fw(p(o + "7s", 'BINPART'.encode('utf-8'))) 260 | fw(p(o + "?", False)) 261 | # 'PARTICLE_POSITIONS' 262 | n = len(d['PARTICLE_POSITIONS']) 263 | fw(p(o + "i", n)) 264 | fw(p(o + "{}d".format(n), *d['PARTICLE_POSITIONS'])) 265 | # 'PARTICLE_SPEEDS' 266 | n = len(d['PARTICLE_SPEEDS']) 267 | fw(p(o + "i", n)) 268 | fw(p(o + "{}d".format(n), *d['PARTICLE_SPEEDS'])) 269 | # 'PARTICLE_RADII' 270 | n = len(d['PARTICLE_RADII']) 271 | fw(p(o + "i", n)) 272 | fw(p(o + "{}d".format(n), *d['PARTICLE_RADII'])) 273 | # 'PARTICLE_NORMALS' 274 | n = len(d['PARTICLE_NORMALS']) 275 | fw(p(o + "i", n)) 276 | fw(p(o + "{}d".format(n), *d['PARTICLE_NORMALS'])) 277 | # 'PARTICLE_IDS' 278 | n = len(d['PARTICLE_IDS']) 279 | fw(p(o + "i", n)) 280 | fw(p(o + "{}i".format(n), *d['PARTICLE_IDS'])) 281 | # 'PARTICLE_UVW' 282 | n = len(d['PARTICLE_UVW']) 283 | fw(p(o + "i", n)) 284 | fw(p(o + "{}d".format(n), *d['PARTICLE_UVW'])) 285 | # end 286 | fw(p(o + "?", False)) 287 | if(os.path.exists(path)): 288 | os.remove(path) 289 | shutil.move("{0}.tmp".format(path), path) 290 | self.path = path 291 | 292 | 293 | class MXSBinParticlesReader(): 294 | def __init__(self, path): 295 | self.offset = 0 296 | with open(path, "rb") as bf: 297 | self.bindata = bf.read() 298 | 299 | def r(f): 300 | d = struct.unpack_from(f, self.bindata, self.offset) 301 | self.offset += struct.calcsize(f) 302 | return d 303 | 304 | # endianness? 305 | signature = 23734338517354818 306 | l = r("q")[0] 309 | self.offset = 0 310 | 311 | if(l == signature): 312 | if(sys.byteorder != "little"): 313 | raise RuntimeError() 314 | self.order = "<" 315 | elif(b == signature): 316 | if(sys.byteorder != "big"): 317 | raise RuntimeError() 318 | self.order = ">" 319 | else: 320 | raise AssertionError("{}: not a MXSBinParticles file".format(self.__class__.__name__)) 321 | o = self.order 322 | # magic 323 | self.magic = r(o + "7s")[0].decode(encoding="utf-8") 324 | if(self.magic != 'BINPART'): 325 | raise RuntimeError() 326 | _ = r(o + "?") 327 | # 'PARTICLE_POSITIONS' 328 | n = r(o + "i")[0] 329 | self.PARTICLE_POSITIONS = r(o + "{}d".format(n)) 330 | # 'PARTICLE_SPEEDS' 331 | n = r(o + "i")[0] 332 | self.PARTICLE_SPEEDS = r(o + "{}d".format(n)) 333 | # 'PARTICLE_RADII' 334 | n = r(o + "i")[0] 335 | self.PARTICLE_RADII = r(o + "{}d".format(n)) 336 | # 'PARTICLE_NORMALS' 337 | n = r(o + "i")[0] 338 | self.PARTICLE_NORMALS = r(o + "{}d".format(n)) 339 | # 'PARTICLE_IDS' 340 | n = r(o + "i")[0] 341 | self.PARTICLE_IDS = r(o + "{}i".format(n)) 342 | # 'PARTICLE_UVW' 343 | n = r(o + "i")[0] 344 | self.PARTICLE_UVW = r(o + "{}d".format(n)) 345 | # eof 346 | e = r(o + "?") 347 | if(self.offset != len(self.bindata)): 348 | raise RuntimeError("expected EOF") 349 | 350 | 351 | class MXSBinWireWriter(): 352 | def __init__(self, path, data): 353 | d = data 354 | o = "@" 355 | with open("{0}.tmp".format(path), 'wb') as f: 356 | p = struct.pack 357 | fw = f.write 358 | # header 359 | fw(p(o + "7s", 'BINWIRE'.encode('utf-8'))) 360 | fw(p(o + "?", False)) 361 | # number of wires 362 | n = len(d) 363 | fw(p(o + "i", n)) 364 | fw(p(o + "?", False)) 365 | # data 366 | for base, pivot, loc, rot, sca in data: 367 | base = tuple(sum(base, ())) 368 | pivot = tuple(sum(pivot, ())) 369 | w = base + pivot + loc + rot + sca 370 | fw(p(o + "33d", *w)) 371 | # end 372 | fw(p(o + "?", False)) 373 | if(os.path.exists(path)): 374 | os.remove(path) 375 | shutil.move("{0}.tmp".format(path), path) 376 | self.path = path 377 | 378 | 379 | class MXSBinWireReader(): 380 | def __init__(self, path): 381 | self.offset = 0 382 | with open(path, "rb") as bf: 383 | self.bindata = bf.read() 384 | 385 | def r(f): 386 | d = struct.unpack_from(f, self.bindata, self.offset) 387 | self.offset += struct.calcsize(f) 388 | return d 389 | 390 | # endianness? 391 | signature = 19512248343873858 392 | l = r("q")[0] 395 | self.offset = 0 396 | if(l == signature): 397 | if(sys.byteorder != "little"): 398 | raise RuntimeError() 399 | self.order = "<" 400 | elif(b == signature): 401 | if(sys.byteorder != "big"): 402 | raise RuntimeError() 403 | self.order = ">" 404 | else: 405 | raise AssertionError("{}: not a MXSBinWire file".format(self.__class__.__name__)) 406 | o = self.order 407 | # magic 408 | self.magic = r(o + "7s")[0].decode(encoding="utf-8") 409 | if(self.magic != 'BINWIRE'): 410 | raise RuntimeError() 411 | _ = r(o + "?") 412 | # number floats 413 | self.num = r(o + "i")[0] 414 | _ = r(o + "?") 415 | self.data = [] 416 | for i in range(self.num): 417 | w = r(o + "33d") 418 | base = w[0:12] 419 | base = [base[i * 3:(i + 1) * 3] for i in range(4)] 420 | pivot = w[12:24] 421 | pivot = [pivot[i * 3:(i + 1) * 3] for i in range(4)] 422 | loc = w[24:27] 423 | rot = w[27:30] 424 | sca = w[30:33] 425 | self.data.append((base, pivot, loc, rot, sca, )) 426 | e = r(o + "?") 427 | if(self.offset != len(self.bindata)): 428 | raise RuntimeError("expected EOF") 429 | 430 | 431 | class MXSBinRefVertsWriter(): 432 | def __init__(self, path, data, ): 433 | o = "@" 434 | with open("{0}.tmp".format(path), 'wb') as f: 435 | p = struct.pack 436 | fw = f.write 437 | # header 438 | fw(p(o + "7s", 'BINREFV'.encode('utf-8'))) 439 | fw(p(o + "?", False)) 440 | # number of objects 441 | fw(p(o + "i", len(data))) 442 | for i in range(len(data)): 443 | d = data[i] 444 | name = d['name'] 445 | base = d['base'] 446 | pivot = d['pivot'] 447 | vertices = d['vertices'] 448 | # name 449 | fw(p(o + "250s", name.encode('utf-8'))) 450 | # base and pivot 451 | fw(p(o + "12d", *[a for b in base for a in b])) 452 | fw(p(o + "12d", *[a for b in pivot for a in b])) 453 | # number of vertices 454 | fw(p(o + "i", len(vertices) * 3)) 455 | # vertices 456 | lv = len(vertices) 457 | fw(p(o + "{}d".format(lv * 3), *[f for v in vertices for f in v])) 458 | fw(p(o + "?", False)) 459 | # swap files 460 | if(os.path.exists(path)): 461 | os.remove(path) 462 | shutil.move("{0}.tmp".format(path), path) 463 | self.path = path 464 | 465 | 466 | class MXSBinRefVertsReader(): 467 | def __init__(self, path): 468 | def r(f, b, o): 469 | d = struct.unpack_from(f, b, o) 470 | o += struct.calcsize(f) 471 | return d, o 472 | 473 | def r0(f, b, o): 474 | d = struct.unpack_from(f, b, o)[0] 475 | o += struct.calcsize(f) 476 | return d, o 477 | 478 | offset = 0 479 | with open(path, "rb") as bf: 480 | buff = bf.read() 481 | # endianness? 482 | signature = 24284111544666434 483 | l, _ = r0("q", buff, 0) 485 | if(l == signature): 486 | if(sys.byteorder != "little"): 487 | raise RuntimeError() 488 | order = "<" 489 | elif(b == signature): 490 | if(sys.byteorder != "big"): 491 | raise RuntimeError() 492 | order = ">" 493 | else: 494 | raise AssertionError("{}: not a MXSBinRefVerts file".format(self.__class__.__name__)) 495 | o = order 496 | # magic 497 | magic, offset = r0(o + "7s", buff, offset) 498 | magic = magic.decode(encoding="utf-8") 499 | if(magic != 'BINREFV'): 500 | raise RuntimeError() 501 | # throwaway 502 | _, offset = r(o + "?", buff, offset) 503 | # number of objects 504 | num_objects, offset = r0(o + "i", buff, offset) 505 | self.data = [] 506 | for i in range(num_objects): 507 | name, offset = r0(o + "250s", buff, offset) 508 | name = name.decode(encoding="utf-8").replace('\x00', '') 509 | b, offset = r(o + "12d", buff, offset) 510 | base = [b[i:i + 3] for i in range(0, len(b), 3)] 511 | p, offset = r(o + "12d", buff, offset) 512 | pivot = [p[i:i + 3] for i in range(0, len(p), 3)] 513 | lv, offset = r0(o + "i", buff, offset) 514 | vs, offset = r(o + "{}d".format(lv), buff, offset) 515 | vertices = [vs[i:i + 3] for i in range(0, len(vs), 3)] 516 | self.data.append({'name': name, 517 | 'base': base, 518 | 'pivot': pivot, 519 | 'vertices': vertices, }) 520 | # throwaway 521 | _, offset = r(o + "?", buff, offset) 522 | # and now.. eof 523 | if(offset != len(buff)): 524 | raise RuntimeError("expected EOF") 525 | -------------------------------------------------------------------------------- /support/read_mxm.py: -------------------------------------------------------------------------------- 1 | #!/Library/Frameworks/Python.framework/Versions/3.5/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # The MIT License (MIT) 5 | # 6 | # Copyright (c) 2015 Jakub Uhlík 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is furnished 13 | # to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all 16 | # copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | # IN THE SOFTWARE. 25 | 26 | import sys 27 | import traceback 28 | import argparse 29 | import textwrap 30 | import os 31 | import json 32 | import shutil 33 | 34 | 35 | quiet = False 36 | LOG_FILE_PATH = None 37 | 38 | 39 | def log(msg, indent=0): 40 | if(quiet): 41 | return 42 | m = "{0}> {1}".format(" " * indent, msg) 43 | print(m) 44 | if(LOG_FILE_PATH is not None): 45 | with open(LOG_FILE_PATH, mode='a', encoding='utf-8', ) as f: 46 | f.write("{}{}".format(m, "\n")) 47 | 48 | 49 | def texture(t): 50 | if(t is None): 51 | return None 52 | if(t.isEmpty()): 53 | return None 54 | 55 | d = {'path': t.getPath(), 56 | 'use_global_map': t.useGlobalMap, 57 | 'channel': t.uvwChannelID, 58 | 'brightness': t.brightness * 100, 59 | 'contrast': t.contrast * 100, 60 | 'saturation': t.saturation * 100, 61 | 'hue': t.hue * 180, 62 | 'rotation': t.rotation, 63 | 'invert': t.invert, 64 | 'interpolation': t.typeInterpolation, 65 | 'use_alpha': t.useAlpha, 66 | 'repeat': [t.scale.x(), t.scale.y()], 67 | 'mirror': [t.uIsMirrored, t.vIsMirrored], 68 | 'offset': [t.offset.x(), t.offset.y()], 69 | 'clamp': [int(t.clampMin * 255), int(t.clampMax * 255)], 70 | 'tiling_units': t.useAbsoluteUnits, 71 | 'tiling_method': [t.uIsTiled, t.vIsTiled], 72 | 'normal_mapping_flip_red': t.normalMappingFlipRed, 73 | 'normal_mapping_flip_green': t.normalMappingFlipGreen, 74 | 'normal_mapping_full_range_blue': t.normalMappingFullRangeBlue, } 75 | 76 | # t.cosA 77 | # t.doGammaCorrection 78 | # t.sinA 79 | # t.theTextureExtensions 80 | 81 | d['procedural'] = [] 82 | if(t.hasProceduralTextures()): 83 | n = t.getProceduralTexturesCount() 84 | for i in range(n): 85 | pd = extension(None, None, t, i) 86 | d['procedural'].append(pd) 87 | 88 | return d 89 | 90 | 91 | def material(s, m): 92 | data = {} 93 | if(m.isNull()): 94 | return data 95 | 96 | # defaults 97 | bsdfd = {'visible': True, 'weight': 100.0, 'weight_map_enabled': False, 'weight_map': None, 'ior': 0, 'complex_ior': "", 98 | 'reflectance_0': (0.6, 0.6, 0.6, ), 'reflectance_0_map_enabled': False, 'reflectance_0_map': None, 99 | 'reflectance_90': (1.0, 1.0, 1.0, ), 'reflectance_90_map_enabled': False, 'reflectance_90_map': None, 100 | 'transmittance': (0.0, 0.0, 0.0), 'transmittance_map_enabled': False, 'transmittance_map': None, 101 | 'attenuation': 1.0, 'attenuation_units': 0, 'nd': 3.0, 'force_fresnel': False, 'k': 0.0, 'abbe': 1.0, 102 | 'r2_enabled': False, 'r2_falloff_angle': 75.0, 'r2_influence': 0.0, 103 | 'roughness': 100.0, 'roughness_map_enabled': False, 'roughness_map': None, 104 | 'bump': 30.0, 'bump_map_enabled': False, 'bump_map': None, 'bump_map_use_normal': False, 'bump_normal': 100.0, 105 | 'anisotropy': 0.0, 'anisotropy_map_enabled': False, 'anisotropy_map': None, 106 | 'anisotropy_angle': 0.0, 'anisotropy_angle_map_enabled': False, 'anisotropy_angle_map': None, 107 | 'scattering': (0.5, 0.5, 0.5, ), 'coef': 0.0, 'asymmetry': 0.0, 108 | 'single_sided': False, 'single_sided_value': 1.0, 'single_sided_map_enabled': False, 'single_sided_map': None, 'single_sided_min': 0.001, 'single_sided_max': 10.0, } 109 | coatingd = {'enabled': False, 110 | 'thickness': 500.0, 'thickness_map_enabled': False, 'thickness_map': None, 'thickness_map_min': 100.0, 'thickness_map_max': 1000.0, 111 | 'ior': 0, 'complex_ior': "", 112 | 'reflectance_0': (0.6, 0.6, 0.6, ), 'reflectance_0_map_enabled': False, 'reflectance_0_map': None, 113 | 'reflectance_90': (1.0, 1.0, 1.0, ), 'reflectance_90_map_enabled': False, 'reflectance_90_map': None, 114 | 'nd': 3.0, 'force_fresnel': False, 'k': 0.0, 'r2_enabled': False, 'r2_falloff_angle': 75.0, } 115 | displacementd = {'enabled': False, 'map': None, 'type': 1, 'subdivision': 5, 'adaptive': False, 'subdivision_method': 0, 116 | 'offset': 0.5, 'smoothing': True, 'uv_interpolation': 2, 'height': 2.0, 'height_units': 0, 117 | 'v3d_preset': 0, 'v3d_transform': 0, 'v3d_rgb_mapping': 0, 'v3d_scale': (1.0, 1.0, 1.0), } 118 | emitterd = {'enabled': False, 'type': 0, 'ies_data': "", 'ies_intensity': 1.0, 119 | 'spot_map_enabled': False, 'spot_map': "", 'spot_cone_angle': 45.0, 'spot_falloff_angle': 10.0, 'spot_falloff_type': 0, 'spot_blur': 1.0, 120 | 'emission': 0, 'color': (1.0, 1.0, 1.0, ), 'color_black_body_enabled': False, 'color_black_body': 6500.0, 121 | 'luminance': 0, 'luminance_power': 40.0, 'luminance_efficacy': 17.6, 'luminance_output': 100.0, 'temperature_value': 6500.0, 122 | 'hdr_map': None, 'hdr_intensity': 1.0, } 123 | layerd = {'visible': True, 'opacity': 100.0, 'opacity_map_enabled': False, 'opacity_map': None, 'blending': 0, } 124 | globald = {'override_map': None, 'bump': 30.0, 'bump_map_enabled': False, 'bump_map': None, 'bump_map_use_normal': False, 'bump_normal': 100.0, 125 | 'dispersion': False, 'shadow': False, 'matte': False, 'priority': 0, 'id': (0.0, 0.0, 0.0), 'active_display_map': None, } 126 | 127 | # structure 128 | structure = [] 129 | nl, _ = m.getNumLayers() 130 | for i in range(nl): 131 | l = m.getLayer(i) 132 | ln, _ = l.getName() 133 | nb, _ = l.getNumBSDFs() 134 | bs = [] 135 | for j in range(nb): 136 | b = l.getBSDF(j) 137 | bn = b.getName() 138 | bs.append([bn, b]) 139 | ls = [ln, l, bs] 140 | structure.append(ls) 141 | 142 | # default data 143 | data['global_props'] = globald.copy() 144 | data['displacement'] = displacementd.copy() 145 | data['layers'] = [] 146 | for i, sl in enumerate(structure): 147 | bsdfs = [] 148 | for j, sb in enumerate(sl[2]): 149 | bsdfs.append({'name': sb[0], 150 | 'bsdf_props': bsdfd.copy(), 151 | 'coating': coatingd.copy(), }) 152 | layer = {'name': sl[0], 153 | 'layer_props': layerd.copy(), 154 | 'bsdfs': bsdfs, 155 | 'emitter': emitterd.copy(), } 156 | data['layers'].append(layer) 157 | 158 | # custom data 159 | def global_props(m, d): 160 | t, _ = m.getGlobalMap() 161 | d['override_map'] = texture(t) 162 | 163 | a, _ = m.getAttribute('bump') 164 | if(a.activeType == MAP_TYPE_BITMAP): 165 | d['bump_map_enabled'] = True 166 | d['bump_map'] = texture(a.textureMap) 167 | d['bump_map_use_normal'] = m.getNormalMapState()[0] 168 | if(d['bump_map_use_normal']): 169 | d['bump_normal'] = a.value 170 | else: 171 | d['bump'] = a.value 172 | else: 173 | d['bump_map_enabled'] = False 174 | d['bump_map'] = None 175 | d['bump_map_use_normal'] = m.getNormalMapState()[0] 176 | if(d['bump_map_use_normal']): 177 | d['bump_normal'] = a.value 178 | else: 179 | d['bump'] = a.value 180 | 181 | d['dispersion'] = m.getDispersion()[0] 182 | d['shadow'] = m.getMatteShadow()[0] 183 | d['matte'] = m.getMatte()[0] 184 | d['priority'] = m.getNestedPriority()[0] 185 | 186 | c, _ = m.getColorID() 187 | d['id'] = [c.r(), c.g(), c.b()] 188 | return d 189 | 190 | data['global_props'] = global_props(m, data['global_props']) 191 | 192 | def displacement(m, d): 193 | if(not m.isDisplacementEnabled()[0]): 194 | return d 195 | d['enabled'] = True 196 | t, _ = m.getDisplacementMap() 197 | d['map'] = texture(t) 198 | 199 | displacementType, subdivisionLevel, smoothness, offset, subdivisionType, interpolationUvType, minLOD, maxLOD, _ = m.getDisplacementCommonParameters() 200 | height, absoluteHeight, adaptive, _ = m.getHeightMapDisplacementParameters() 201 | scale, transformType, mapping, preset, _ = m.getVectorDisplacementParameters() 202 | 203 | d['type'] = displacementType 204 | d['subdivision'] = subdivisionLevel 205 | d['adaptive'] = adaptive 206 | d['subdivision_method'] = subdivisionType 207 | d['offset'] = offset 208 | d['smoothing'] = bool(smoothness) 209 | d['uv_interpolation'] = interpolationUvType 210 | d['height'] = height 211 | d['height_units'] = absoluteHeight 212 | d['v3d_preset'] = preset 213 | d['v3d_transform'] = transformType 214 | d['v3d_rgb_mapping'] = mapping 215 | d['v3d_scale'] = (scale.x(), scale.y(), scale.z(), ) 216 | 217 | return d 218 | 219 | data['displacement'] = displacement(m, data['displacement']) 220 | 221 | def cattribute_rgb(a): 222 | if(a.activeType == MAP_TYPE_BITMAP): 223 | c = (a.rgb.r(), a.rgb.g(), a.rgb.b()) 224 | e = True 225 | m = texture(a.textureMap) 226 | else: 227 | c = (a.rgb.r(), a.rgb.g(), a.rgb.b()) 228 | e = False 229 | m = None 230 | return c, e, m 231 | 232 | def cattribute_value(a): 233 | if(a.activeType == MAP_TYPE_BITMAP): 234 | v = a.value 235 | e = True 236 | m = texture(a.textureMap) 237 | else: 238 | v = a.value 239 | e = False 240 | m = None 241 | return v, e, m 242 | 243 | def layer_props(l, d): 244 | d['visible'] = l.getEnabled()[0] 245 | d['blending'] = l.getStackedBlendingMode()[0] 246 | a, _ = l.getAttribute('weight') 247 | if(a.activeType == MAP_TYPE_BITMAP): 248 | d['opacity'] = a.value 249 | d['opacity_map_enabled'] = True 250 | d['opacity_map'] = texture(a.textureMap) 251 | else: 252 | d['opacity'] = a.value 253 | d['opacity_map_enabled'] = False 254 | d['opacity_map'] = None 255 | return d 256 | 257 | def emitter(l, d): 258 | e = l.getEmitter() 259 | if(e.isNull()): 260 | d['enabled'] = False 261 | return d 262 | 263 | d['enabled'] = True 264 | d['type'] = e.getLobeType()[0] 265 | 266 | d['ies_data'] = e.getLobeIES() 267 | d['ies_intensity'] = e.getIESLobeIntensity()[0] 268 | 269 | t, _ = e.getLobeImageProjectedMap() 270 | d['spot_map_enabled'] = (not t.isEmpty()) 271 | 272 | d['spot_map'] = texture(t) 273 | d['spot_cone_angle'] = e.getSpotConeAngle()[0] 274 | d['spot_falloff_angle'] = e.getSpotFallOffAngle()[0] 275 | d['spot_falloff_type'] = e.getSpotFallOffType()[0] 276 | d['spot_blur'] = e.getSpotBlur()[0] 277 | 278 | d['emission'] = e.getActiveEmissionType()[0] 279 | ep, _ = e.getPair() 280 | colorType, units, _ = e.getActivePair() 281 | 282 | d['color'] = (ep.rgb.r(), ep.rgb.g(), ep.rgb.b(), ) 283 | d['color_black_body'] = ep.temperature 284 | 285 | d['luminance'] = units 286 | if(units == EMISSION_UNITS_WATTS_AND_LUMINOUS_EFFICACY): 287 | d['luminance_power'] = ep.watts 288 | d['luminance_efficacy'] = ep.luminousEfficacy 289 | elif(units == EMISSION_UNITS_LUMINOUS_POWER): 290 | d['luminance_output'] = ep.luminousPower 291 | elif(units == EMISSION_UNITS_ILLUMINANCE): 292 | d['luminance_output'] = ep.illuminance 293 | elif(units == EMISSION_UNITS_LUMINOUS_INTENSITY): 294 | d['luminance_output'] = ep.luminousIntensity 295 | elif(units == EMISSION_UNITS_LUMINANCE): 296 | d['luminance_output'] = ep.luminance 297 | if(colorType == EMISSION_COLOR_TEMPERATURE): 298 | d['color_black_body_enabled'] = True 299 | 300 | d['temperature_value'] = e.getTemperature()[0] 301 | a, _ = e.getMXI() 302 | if(a.activeType == MAP_TYPE_BITMAP): 303 | d['hdr_map'] = texture(a.textureMap) 304 | d['hdr_intensity'] = a.value 305 | else: 306 | d['hdr_map'] = None 307 | d['hdr_intensity'] = a.value 308 | 309 | return d 310 | 311 | def bsdf_props(b, d): 312 | d['visible'] = b.getState()[0] 313 | 314 | a, _ = b.getWeight() 315 | if(a.activeType == MAP_TYPE_BITMAP): 316 | d['weight_map_enabled'] = True 317 | d['weight'] = a.value 318 | d['weight_map'] = texture(a.textureMap) 319 | else: 320 | d['weight_map_enabled'] = False 321 | d['weight'] = a.value 322 | d['weight_map'] = None 323 | 324 | r = b.getReflectance() 325 | d['ior'] = r.getActiveIorMode()[0] 326 | d['complex_ior'] = r.getComplexIor() 327 | 328 | d['reflectance_0'], d['reflectance_0_map_enabled'], d['reflectance_0_map'] = cattribute_rgb(r.getAttribute('color')[0]) 329 | d['reflectance_90'], d['reflectance_90_map_enabled'], d['reflectance_90_map'] = cattribute_rgb(r.getAttribute('color.tangential')[0]) 330 | d['transmittance'], d['transmittance_map_enabled'], d['transmittance_map'] = cattribute_rgb(r.getAttribute('transmittance.color')[0]) 331 | d['attenuation_units'], d['attenuation'] = r.getAbsorptionDistance() 332 | d['nd'], d['abbe'], _ = r.getIOR() 333 | d['force_fresnel'], _ = r.getForceFresnel() 334 | d['k'], _ = r.getConductor() 335 | d['r2_falloff_angle'], d['r2_influence'], d['r2_enabled'], _ = r.getFresnelCustom() 336 | 337 | d['roughness'], d['roughness_map_enabled'], d['roughness_map'] = cattribute_value(b.getAttribute('roughness')[0]) 338 | d['bump_map_use_normal'] = b.getNormalMapState()[0] 339 | if(d['bump_map_use_normal']): 340 | d['bump_normal'], d['bump_map_enabled'], d['bump_map'] = cattribute_value(b.getAttribute('bump')[0]) 341 | else: 342 | d['bump'], d['bump_map_enabled'], d['bump_map'] = cattribute_value(b.getAttribute('bump')[0]) 343 | d['anisotropy'], d['anisotropy_map_enabled'], d['anisotropy_map'] = cattribute_value(b.getAttribute('anisotropy')[0]) 344 | d['anisotropy_angle'], d['anisotropy_angle_map_enabled'], d['anisotropy_angle_map'] = cattribute_value(b.getAttribute('angle')[0]) 345 | 346 | a, _ = r.getAttribute('scattering') 347 | d['scattering'] = (a.rgb.r(), a.rgb.g(), a.rgb.b(), ) 348 | d['coef'], d['asymmetry'], d['single_sided'], _ = r.getScatteringParameters() 349 | d['single_sided_value'], d['single_sided_map_enabled'], d['single_sided_map'] = cattribute_value(r.getScatteringThickness()[0]) 350 | d['single_sided_min'], d['single_sided_max'], _ = r.getScatteringThicknessRange() 351 | 352 | return d 353 | 354 | def coating(b, d): 355 | nc, _ = b.getNumCoatings() 356 | if(nc > 0): 357 | c = b.getCoating(0) 358 | else: 359 | d['enabled'] = False 360 | return d 361 | 362 | d['enabled'] = True 363 | d['thickness'], d['thickness_map_enabled'], d['thickness_map'] = cattribute_value(c.getThickness()[0]) 364 | d['thickness_map_min'], d['thickness_map_max'], _ = c.getThicknessRange() 365 | 366 | r = c.getReflectance() 367 | d['ior'] = r.getActiveIorMode()[0] 368 | d['complex_ior'] = r.getComplexIor() 369 | 370 | d['reflectance_0'], d['reflectance_0_map_enabled'], d['reflectance_0_map'] = cattribute_rgb(r.getAttribute('color')[0]) 371 | d['reflectance_90'], d['reflectance_90_map_enabled'], d['reflectance_90_map'] = cattribute_rgb(r.getAttribute('color.tangential')[0]) 372 | 373 | d['nd'], _, _ = r.getIOR() 374 | d['force_fresnel'], _ = r.getForceFresnel() 375 | d['k'], _ = r.getConductor() 376 | d['r2_falloff_angle'], _, d['r2_enabled'], _ = r.getFresnelCustom() 377 | 378 | return d 379 | 380 | for i, sl in enumerate(structure): 381 | l = sl[1] 382 | data['layers'][i]['layer_props'] = layer_props(l, data['layers'][i]['layer_props']) 383 | data['layers'][i]['emitter'] = emitter(l, data['layers'][i]['emitter']) 384 | for j, bs in enumerate(sl[2]): 385 | b = bs[1] 386 | data['layers'][i]['bsdfs'][j]['bsdf_props'] = bsdf_props(b, data['layers'][i]['bsdfs'][j]['bsdf_props']) 387 | data['layers'][i]['bsdfs'][j]['coating'] = coating(b, data['layers'][i]['bsdfs'][j]['coating']) 388 | 389 | return data 390 | 391 | 392 | def extension(s, m, pt=None, pi=None, ): 393 | def texture(t): 394 | if(t is None): 395 | return None 396 | if(t.isEmpty()): 397 | return None 398 | d = {'path': t.getPath(), 399 | 'use_global_map': t.useGlobalMap, 400 | 'channel': t.uvwChannelID, 401 | 'brightness': t.brightness * 100, 402 | 'contrast': t.contrast * 100, 403 | 'saturation': t.saturation * 100, 404 | 'hue': t.hue * 180, 405 | 'rotation': t.rotation, 406 | 'invert': t.invert, 407 | 'interpolation': t.typeInterpolation, 408 | 'use_alpha': t.useAlpha, 409 | 'repeat': [t.scale.x(), t.scale.y()], 410 | 'mirror': [t.uIsMirrored, t.vIsMirrored], 411 | 'offset': [t.offset.x(), t.offset.y()], 412 | 'clamp': [int(t.clampMin * 255), int(t.clampMax * 255)], 413 | 'tiling_units': t.useAbsoluteUnits, 414 | 'tiling_method': [t.uIsTiled, t.vIsTiled], 415 | 'normal_mapping_flip_red': t.normalMappingFlipRed, 416 | 'normal_mapping_flip_green': t.normalMappingFlipGreen, 417 | 'normal_mapping_full_range_blue': t.normalMappingFullRangeBlue, } 418 | return d 419 | 420 | def mxparamlistarray(v): 421 | return None 422 | 423 | def rgb(v): 424 | return (v.r(), v.g(), v.b()) 425 | 426 | if(pt is not None and pi is not None): 427 | params = pt.getProceduralTexture(pi) 428 | else: 429 | params, _ = m.getMaterialModifierExtensionParams() 430 | types = [(0, 'UCHAR', params.getByte, ), 431 | (1, 'UINT', params.getUInt, ), 432 | (2, 'INT', params.getInt, ), 433 | (3, 'FLOAT', params.getFloat, ), 434 | (4, 'DOUBLE', params.getDouble, ), 435 | (5, 'STRING', params.getString, ), 436 | (6, 'FLOATARRAY', params.getFloatArray, ), 437 | (7, 'DOUBLEARRAY', params.getDoubleArray, ), 438 | (8, 'BYTEARRAY', params.getByteArray, ), 439 | (9, 'INTARRAY', params.getIntArray, ), 440 | (10, 'MXPARAMLIST', params.getTextureMap, ), 441 | (11, 'MXPARAMLISTARRAY', mxparamlistarray, ), 442 | (12, 'RGB', params.getRgb, ), ] 443 | 444 | d = {} 445 | for i in range(params.getNumItems()): 446 | name, data, _, _, data_type, _, data_count, _ = params.getByIndex(i) 447 | _, _, f = types[data_type] 448 | k = name 449 | if(data_type not in [10, 11, 12]): 450 | v, _ = f(name) 451 | else: 452 | if(data_type == 10): 453 | v = texture(f(name)[0]) 454 | elif(data_type == 11): 455 | pass 456 | elif(data_type == 12): 457 | v = rgb(f(name)[0]) 458 | d[k] = v 459 | return d 460 | 461 | 462 | def main(args): 463 | log("mxm to dict:", 1) 464 | p = args.mxm_path 465 | s = Cmaxwell(mwcallback) 466 | log("reading mxm from: {0}".format(p), 2) 467 | m = s.readMaterial(p) 468 | data = material(s, m) 469 | if(m.hasMaterialModifier()): 470 | data['extension'] = extension(s, m) 471 | log("serializing..", 2) 472 | p = args.data_path 473 | with open("{0}.tmp".format(p), 'w', encoding='utf-8', ) as f: 474 | json.dump(data, f, skipkeys=False, ensure_ascii=False, indent=4, ) 475 | if(os.path.exists(p)): 476 | os.remove(p) 477 | shutil.move("{0}.tmp".format(p), p) 478 | log("done.", 2) 479 | 480 | 481 | if __name__ == "__main__": 482 | parser = argparse.ArgumentParser(description=textwrap.dedent('''mxm to dict'''), epilog='', 483 | formatter_class=argparse.RawDescriptionHelpFormatter, add_help=True, ) 484 | parser.add_argument('pymaxwell_path', type=str, help='path to directory containing pymaxwell') 485 | parser.add_argument('log_file', type=str, help='path to log file') 486 | parser.add_argument('mxm_path', type=str, help='path to .mxm') 487 | parser.add_argument('data_path', type=str, help='path to serialized data') 488 | args = parser.parse_args() 489 | 490 | PYMAXWELL_PATH = args.pymaxwell_path 491 | 492 | try: 493 | from pymaxwell import * 494 | except ImportError: 495 | if(not os.path.exists(PYMAXWELL_PATH)): 496 | raise OSError("pymaxwell for python 3.5 does not exist ({})".format(PYMAXWELL_PATH)) 497 | sys.path.insert(0, PYMAXWELL_PATH) 498 | from pymaxwell import * 499 | 500 | LOG_FILE_PATH = args.log_file 501 | 502 | try: 503 | main(args) 504 | except Exception as e: 505 | m = traceback.format_exc() 506 | log(m) 507 | sys.exit(1) 508 | sys.exit(0) 509 | -------------------------------------------------------------------------------- /impmxs.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import os 20 | import math 21 | import shlex 22 | import subprocess 23 | import uuid 24 | import shutil 25 | import struct 26 | import json 27 | import sys 28 | import re 29 | 30 | import bpy 31 | from mathutils import Matrix, Vector 32 | from bpy_extras import io_utils 33 | import bmesh 34 | 35 | from .log import log, LogStyles 36 | from . import utils 37 | from . import maths 38 | from . import system 39 | from . import rfbin 40 | from . import mxs 41 | 42 | 43 | class MXSImportMacOSX(): 44 | def __init__(self, mxs_path, emitters, objects, cameras, sun, keep_intermediates=False, ): 45 | self.TEMPLATE = system.check_for_import_template() 46 | self.mxs_path = os.path.realpath(mxs_path) 47 | self.import_emitters = emitters 48 | self.import_objects = objects 49 | self.import_cameras = cameras 50 | self.import_sun = sun 51 | self.keep_intermediates = keep_intermediates 52 | self._import() 53 | 54 | def _import(self): 55 | log("{0} {1} {0}".format("-" * 30, self.__class__.__name__), 0, LogStyles.MESSAGE, prefix="", ) 56 | 57 | self.uuid = uuid.uuid1() 58 | 59 | h, t = os.path.split(self.mxs_path) 60 | n, e = os.path.splitext(t) 61 | if(bpy.data.filepath == ""): 62 | p = os.path.join(h, '{}-tmp-import_scene-{}'.format(n, self.uuid)) 63 | self.tmp_dir = utils.tmp_dir(override_path=p) 64 | else: 65 | self.tmp_dir = utils.tmp_dir(purpose='import_scene', uid=self.uuid, use_blend_name=True, ) 66 | 67 | self.scene_data_name = "{0}-{1}.json".format(n, self.uuid) 68 | self.script_name = "{0}-{1}.py".format(n, self.uuid) 69 | self.scene_data_path = os.path.join(self.tmp_dir, self.scene_data_name) 70 | 71 | log("executing script..", 1, LogStyles.MESSAGE) 72 | self._pymaxwell() 73 | log("processing objects..", 1, LogStyles.MESSAGE) 74 | self._process() 75 | log("cleanup..", 1, LogStyles.MESSAGE) 76 | self._cleanup() 77 | log("done.", 1, LogStyles.MESSAGE) 78 | 79 | def _process(self): 80 | """Loop over all data from pymaxwell and create corresponding blender objects.""" 81 | data = None 82 | 83 | if(not os.path.exists(self.scene_data_path)): 84 | raise RuntimeError("Protected MXS?") 85 | 86 | with open(self.scene_data_path, 'r') as f: 87 | data = json.load(f) 88 | 89 | for d in data: 90 | t = None 91 | try: 92 | t = d['type'] 93 | except KeyError: 94 | log("element without type: {0}".format(d), 1, LogStyles.WARNING) 95 | if(d['type'] == 'EMPTY'): 96 | o = self._empty(d) 97 | 98 | if(d['referenced_mxs']): 99 | o.maxwell_render.reference.enabled = True 100 | o.maxwell_render.reference.path = d['referenced_mxs_path'] 101 | 102 | d['created'] = o 103 | elif(d['type'] == 'MESH'): 104 | o = self._mesh(d) 105 | d['created'] = o 106 | elif(d['type'] == 'INSTANCE'): 107 | o = self._instance(d) 108 | d['created'] = o 109 | elif(d['type'] == 'CAMERA'): 110 | o = self._camera(d) 111 | d['created'] = o 112 | elif(d['type'] == 'SUN'): 113 | o = self._sun(d) 114 | d['created'] = o 115 | else: 116 | log("unknown type: {0}".format(t), 1, LogStyles.WARNING) 117 | 118 | # log("setting object hierarchy..", 1, LogStyles.MESSAGE) 119 | # self._hierarchy(data) 120 | log("setting object transformations..", 1, LogStyles.MESSAGE) 121 | self._transformations(data) 122 | log("setting object hierarchy..", 1, LogStyles.MESSAGE) 123 | self._hierarchy(data) 124 | log("finalizing..", 1, LogStyles.MESSAGE) 125 | self._finalize() 126 | 127 | def _empty(self, d): 128 | n = d['name'] 129 | log("empty: {0}".format(n), 2) 130 | o = utils.add_object2(n, None) 131 | return o 132 | 133 | def _mesh(self, d): 134 | nm = d['name'] 135 | log("mesh: {0}".format(nm), 2) 136 | 137 | l = len(d['vertices']) + len(d['triangles']) 138 | nuv = len(d['trianglesUVW']) 139 | for i in range(nuv): 140 | l += len(d['trianglesUVW'][i]) 141 | 142 | # mesh 143 | me = bpy.data.meshes.new(nm) 144 | vs = [] 145 | fs = [] 146 | sf = [] 147 | for v in d['vertices']: 148 | vs.append(v) 149 | for t in d['triangles']: 150 | fs.append((t[0], t[1], t[2])) 151 | if(t[3] == t[4] == t[5]): 152 | sf.append(False) 153 | else: 154 | sf.append(True) 155 | 156 | me.from_pydata(vs, [], fs) 157 | for i, p in enumerate(me.polygons): 158 | p.use_smooth = sf[i] 159 | 160 | nuv = len(d['trianglesUVW']) 161 | for i in range(nuv): 162 | muv = d['trianglesUVW'][i] 163 | uv = me.uv_textures.new(name="uv{0}".format(i)) 164 | uvloops = me.uv_layers[i] 165 | for j, p in enumerate(me.polygons): 166 | li = p.loop_indices 167 | t = muv[j] 168 | v0 = (t[0], t[1]) 169 | v1 = (t[3], t[4]) 170 | v2 = (t[6], t[7]) 171 | # no need to loop, maxwell meshes are always(?) triangles 172 | uvloops.data[li[0]].uv = v0 173 | uvloops.data[li[1]].uv = v1 174 | uvloops.data[li[2]].uv = v2 175 | 176 | # mr90 = Matrix.Rotation(math.radians(90.0), 4, 'X') 177 | # me.transform(mr90) 178 | 179 | o = utils.add_object2(nm, me) 180 | 181 | return o 182 | 183 | def _instance(self, d): 184 | log("instance: {0}".format(d['name']), 2) 185 | o = None 186 | io = bpy.data.objects[d['instanced']] 187 | if(io.type == 'MESH'): 188 | m = bpy.data.meshes[d['instanced']] 189 | o = utils.add_object2(d['name'], m) 190 | elif(io.type == 'EMPTY'): 191 | o = utils.add_object2(d['name'], None) 192 | if(d['referenced_mxs']): 193 | o.maxwell_render.reference.enabled = True 194 | o.maxwell_render.reference.path = d['referenced_mxs_path'] 195 | return o 196 | 197 | def _camera(self, d): 198 | log("camera: {0}".format(d['name']), 2) 199 | 200 | mx_type = d['type'] 201 | mx_name = d['name'] 202 | mx_origin = d['origin'] 203 | mx_focal_point = d['focal_point'] 204 | mx_up = d['up'] 205 | mx_focal_length = d['focal_length'] 206 | mx_sensor_fit = d['sensor_fit'] 207 | mx_film_width = d['film_width'] 208 | mx_film_height = d['film_height'] 209 | mx_xres = d['x_res'] 210 | mx_yres = d['y_res'] 211 | mx_active = d['active'] 212 | mx_zclip = d['zclip'] 213 | mx_zclip_near = d['zclip_near'] 214 | mx_zclip_far = d['zclip_far'] 215 | mx_shift_x = d['shift_x'] 216 | mx_shift_y = d['shift_y'] 217 | 218 | # convert axes 219 | cm = io_utils.axis_conversion(from_forward='-Y', to_forward='Z', from_up='Z', to_up='Y') 220 | cm.to_4x4() 221 | eye = Vector(mx_origin) * cm 222 | target = Vector(mx_focal_point) * cm 223 | up = Vector(mx_up) * cm 224 | 225 | cd = bpy.data.cameras.new(mx_name) 226 | c = bpy.data.objects.new(mx_name, cd) 227 | bpy.context.scene.objects.link(c) 228 | 229 | m = self._matrix_look_at(eye, target, up) 230 | c.matrix_world = m 231 | 232 | # distance 233 | mx_dof_distance = self._distance(mx_origin, mx_focal_point) 234 | 235 | # camera properties 236 | cd.lens = mx_focal_length 237 | cd.dof_distance = mx_dof_distance 238 | cd.sensor_fit = mx_sensor_fit 239 | cd.sensor_width = mx_film_width 240 | cd.sensor_height = mx_film_height 241 | 242 | cd.clip_start = mx_zclip_near 243 | cd.clip_end = mx_zclip_far 244 | cd.shift_x = mx_shift_x / 10.0 245 | cd.shift_y = mx_shift_y / 10.0 246 | 247 | if(mx_active): 248 | render = bpy.context.scene.render 249 | render.resolution_x = mx_xres 250 | render.resolution_y = mx_yres 251 | render.resolution_percentage = 100 252 | bpy.context.scene.camera = c 253 | 254 | return c 255 | 256 | def _sun(self, d): 257 | n = d['name'] 258 | log("sun: {0}".format(n), 2) 259 | l = bpy.data.lamps.new(n, 'SUN') 260 | o = utils.add_object2(n, l) 261 | v = Vector(d['xyz']) 262 | mrx90 = Matrix.Rotation(math.radians(90.0), 4, 'X') 263 | v.rotate(mrx90) 264 | m = self._matrix_look_at(v, Vector((0.0, 0.0, 0.0)), Vector((0.0, 0.0, 1.0))) 265 | o.matrix_world = m 266 | 267 | # align sun ray (which is 25bu long) end with scene center 268 | d = 25 269 | l, r, s = m.decompose() 270 | n = Vector((0.0, 0.0, 1.0)) 271 | n.rotate(r) 272 | loc = maths.shift_vert_along_normal(l, n, d - 1) 273 | o.location = loc 274 | 275 | return o 276 | 277 | def _hierarchy(self, data): 278 | """Set parent child relationships in scene.""" 279 | types = ['MESH', 'INSTANCE', 'EMPTY'] 280 | for d in data: 281 | t = d['type'] 282 | if(t in types): 283 | # o = self._get_object_by_name(d['name']) 284 | o = d['created'] 285 | if(d['parent'] is not None): 286 | # p = self._get_object_by_name(d['parent']) 287 | p = None 288 | for q in data: 289 | if(q['name'] == d['parent']): 290 | p = q['created'] 291 | break 292 | o.parent = p 293 | 294 | def _transformations(self, data): 295 | """Apply transformation to all objects.""" 296 | types = ['MESH', 'INSTANCE', 'EMPTY'] 297 | mrx90 = Matrix.Rotation(math.radians(90.0), 4, 'X') 298 | mrxneg90 = Matrix.Rotation(math.radians(-90.0), 4, 'X') 299 | for d in data: 300 | t = d['type'] 301 | if(t in types): 302 | o = d['created'] 303 | if(o.type == 'MESH'): 304 | if(d['type'] != 'INSTANCE'): 305 | o.data.transform(mrx90) 306 | 307 | m = self._base_and_pivot_to_matrix(d) 308 | o.matrix_local = m * mrxneg90 309 | 310 | def _distance(self, a, b): 311 | ax, ay, az = a 312 | bx, by, bz = b 313 | return ((ax - bx) ** 2 + (ay - by) ** 2 + (az - bz) ** 2) ** 0.5 314 | 315 | def _base_and_pivot_to_matrix(self, d): 316 | ''' 317 | am = io_utils.axis_conversion(from_forward='-Z', from_up='Y', to_forward='Y', to_up='Z', ).to_4x4() 318 | b = d['base'] 319 | o = b[0] 320 | x = b[1] 321 | y = b[2] 322 | z = b[3] 323 | bm = Matrix([(x[0], y[0], z[0], o[0]), (x[1], y[1], z[1], o[1]), (x[2], y[2], z[2], o[2]), (0.0, 0.0, 0.0, 1.0)]) 324 | p = d['pivot'] 325 | o = p[0] 326 | x = p[1] 327 | y = p[2] 328 | z = p[3] 329 | pm = Matrix([(x[0], y[0], z[0], o[0]), (x[1], y[1], z[1], o[1]), (x[2], y[2], z[2], o[2]), (0.0, 0.0, 0.0, 1.0)]) 330 | mat = am * bm * pm 331 | obj.matrix_world = mat 332 | ''' 333 | am = io_utils.axis_conversion(from_forward='-Z', from_up='Y', to_forward='Y', to_up='Z', ).to_4x4() 334 | 335 | def cbase_to_matrix4(cbase): 336 | o = cbase[0] 337 | x = cbase[1] 338 | y = cbase[2] 339 | z = cbase[3] 340 | m = Matrix([(x[0], y[0], z[0], o[0]), 341 | (x[1], y[1], z[1], o[1]), 342 | (x[2], y[2], z[2], o[2]), 343 | (0.0, 0.0, 0.0, 1.0)]) 344 | return m 345 | 346 | bm = cbase_to_matrix4(d['base']) 347 | pm = cbase_to_matrix4(d['pivot']) 348 | m = am * bm * pm 349 | return m 350 | 351 | def _matrix_look_at(self, eye, target, up): 352 | # https://github.com/mono/opentk/blob/master/Source/OpenTK/Math/Matrix4.cs 353 | 354 | z = eye - target 355 | x = up.cross(z) 356 | y = z.cross(x) 357 | 358 | x.normalize() 359 | y.normalize() 360 | z.normalize() 361 | 362 | rot = Matrix() 363 | rot[0][0] = x[0] 364 | rot[0][1] = y[0] 365 | rot[0][2] = z[0] 366 | rot[0][3] = 0 367 | rot[1][0] = x[1] 368 | rot[1][1] = y[1] 369 | rot[1][2] = z[1] 370 | rot[1][3] = 0 371 | rot[2][0] = x[2] 372 | rot[2][1] = y[2] 373 | rot[2][2] = z[2] 374 | rot[2][3] = 0 375 | 376 | # eye not need to be minus cmp to opentk 377 | # perhaps opentk has z inverse axis 378 | tran = Matrix.Translation(eye) 379 | return tran * rot 380 | 381 | def _pymaxwell(self): 382 | # generate script 383 | self.script_path = os.path.join(self.tmp_dir, self.script_name) 384 | with open(self.script_path, mode='w', encoding='utf-8') as f: 385 | # read template 386 | with open(self.TEMPLATE, encoding='utf-8') as t: 387 | code = "".join(t.readlines()) 388 | # write template to a new file 389 | f.write(code) 390 | 391 | if(system.PLATFORM == 'Darwin'): 392 | system.python34_run_script_helper_import(self.script_path, self.mxs_path, self.scene_data_path, self.import_emitters, self.import_objects, self.import_cameras, self.import_sun, ) 393 | elif(system.PLATFORM == 'Linux'): 394 | pass 395 | elif(system.PLATFORM == 'Windows'): 396 | pass 397 | else: 398 | pass 399 | 400 | def _finalize(self): 401 | # maybe i am just setting normals badly? how to find out? 402 | 403 | # remove strange smooth shading >> cycle edit mode on all meshes.. 404 | # i have no idea why this happens, never happended before, but this seems to fix that. 405 | cycled_meshes = [] 406 | for o in bpy.data.objects: 407 | if(o.type == 'MESH'): 408 | # skip instances, apply only on first mesh multiuser encountered.. 409 | if(o.data in cycled_meshes): 410 | pass 411 | else: 412 | bpy.ops.object.select_all(action='DESELECT') 413 | o.select = True 414 | bpy.context.scene.objects.active = o 415 | bpy.ops.object.mode_set(mode='EDIT') 416 | bpy.ops.object.mode_set(mode='OBJECT') 417 | cycled_meshes.append(o.data) 418 | 419 | def _cleanup(self): 420 | if(self.keep_intermediates): 421 | return 422 | 423 | # remove script, data, temp directory 424 | def rm(p): 425 | if(os.path.exists(p)): 426 | os.remove(p) 427 | else: 428 | log("_cleanup(): {} does not exist?".format(p), 1, LogStyles.WARNING, ) 429 | 430 | rm(self.script_path) 431 | rm(self.scene_data_path) 432 | 433 | if(os.path.exists(self.tmp_dir)): 434 | os.rmdir(self.tmp_dir) 435 | else: 436 | log("_cleanup(): {} does not exist?".format(self.tmp_dir), 1, LogStyles.WARNING, ) 437 | 438 | 439 | class MXSImportWinLin(): 440 | def __init__(self, mxs_path, emitters=True, objects=True, cameras=True, sun=True, ): 441 | self.mxs_path = os.path.realpath(mxs_path) 442 | self.import_emitters = emitters 443 | self.import_objects = objects 444 | if(self.import_objects): 445 | self.import_emitters = False 446 | self.import_cameras = cameras 447 | self.import_sun = sun 448 | self._import() 449 | 450 | def _import(self): 451 | log("{0} {1} {0}".format("-" * 30, self.__class__.__name__), 0, LogStyles.MESSAGE, prefix="", ) 452 | reader = mxs.MXSReader(self.mxs_path) 453 | 454 | if(self.import_objects or self.import_emitters): 455 | data = reader.objects(self.import_emitters) 456 | for d in data: 457 | t = None 458 | try: 459 | t = d['type'] 460 | except KeyError: 461 | log("element without type: {0}".format(d), 1, LogStyles.WARNING) 462 | if(t is None): 463 | continue 464 | if(t == 'EMPTY'): 465 | o = self._empty(d) 466 | 467 | if(d['referenced_mxs']): 468 | o.maxwell_render.reference.enabled = True 469 | o.maxwell_render.reference.path = d['referenced_mxs_path'] 470 | 471 | d['created'] = o 472 | elif(t == 'MESH'): 473 | o = self._mesh(d) 474 | d['created'] = o 475 | elif(t == 'INSTANCE'): 476 | o = self._instance(d) 477 | d['created'] = o 478 | else: 479 | log("unknown type: {0}".format(t), 1, LogStyles.WARNING) 480 | 481 | log("setting object hierarchy..", 1, LogStyles.MESSAGE) 482 | self._hierarchy(data) 483 | log("setting object transformations..", 1, LogStyles.MESSAGE) 484 | self._transformations(data) 485 | log("finalizing..", 1, LogStyles.MESSAGE) 486 | self._finalize() 487 | 488 | if(self.import_cameras): 489 | data = reader.cameras() 490 | for d in data: 491 | t = None 492 | try: 493 | t = d['type'] 494 | except KeyError: 495 | log("element without type: {0}".format(d), 1, LogStyles.WARNING) 496 | if(t is None): 497 | continue 498 | if(t == 'CAMERA'): 499 | o = self._camera(d) 500 | else: 501 | log("unknown type: {0}".format(t), 1, LogStyles.WARNING) 502 | 503 | if(self.import_sun): 504 | data = reader.sun() 505 | for d in data: 506 | t = None 507 | try: 508 | t = d['type'] 509 | except KeyError: 510 | log("element without type: {0}".format(d), 1, LogStyles.WARNING) 511 | if(t is None): 512 | continue 513 | if(t == 'SUN'): 514 | o = self._sun(d) 515 | else: 516 | log("unknown type: {0}".format(t), 1, LogStyles.WARNING) 517 | 518 | log("done.", 1) 519 | 520 | def _empty(self, d): 521 | n = d['name'] 522 | log("empty: {0}".format(n), 2) 523 | o = utils.add_object2(n, None) 524 | return o 525 | 526 | def _mesh(self, d): 527 | nm = d['name'] 528 | log("mesh: {0}".format(nm), 2) 529 | 530 | l = len(d['vertices']) + len(d['triangles']) 531 | nuv = len(d['trianglesUVW']) 532 | for i in range(nuv): 533 | l += len(d['trianglesUVW'][i]) 534 | 535 | me = bpy.data.meshes.new(nm) 536 | vs = [] 537 | fs = [] 538 | sf = [] 539 | for v in d['vertices']: 540 | vs.append(v) 541 | for t in d['triangles']: 542 | fs.append((t[0], t[1], t[2])) 543 | if(t[3] == t[4] == t[5]): 544 | sf.append(False) 545 | else: 546 | sf.append(True) 547 | 548 | me.from_pydata(vs, [], fs) 549 | for i, p in enumerate(me.polygons): 550 | p.use_smooth = sf[i] 551 | 552 | nuv = len(d['trianglesUVW']) 553 | for i in range(nuv): 554 | muv = d['trianglesUVW'][i] 555 | uv = me.uv_textures.new(name="uv{0}".format(i)) 556 | uvloops = me.uv_layers[i] 557 | for j, p in enumerate(me.polygons): 558 | li = p.loop_indices 559 | t = muv[j] 560 | v0 = (t[0], t[1]) 561 | v1 = (t[3], t[4]) 562 | v2 = (t[6], t[7]) 563 | # no need to loop, maxwell meshes are always(?) triangles 564 | uvloops.data[li[0]].uv = v0 565 | uvloops.data[li[1]].uv = v1 566 | uvloops.data[li[2]].uv = v2 567 | 568 | o = utils.add_object2(nm, me) 569 | return o 570 | 571 | def _instance(self, d): 572 | log("instance: {0}".format(d['name']), 2) 573 | o = None 574 | io = bpy.data.objects[d['instanced']] 575 | if(io.type == 'MESH'): 576 | m = bpy.data.meshes[d['instanced']] 577 | o = utils.add_object2(d['name'], m) 578 | elif(io.type == 'EMPTY'): 579 | o = utils.add_object2(d['name'], None) 580 | if(d['referenced_mxs']): 581 | o.maxwell_render.reference.enabled = True 582 | o.maxwell_render.reference.path = d['referenced_mxs_path'] 583 | return o 584 | 585 | def _camera(self, d): 586 | log("camera: {0}".format(d['name']), 2) 587 | 588 | mx_type = d['type'] 589 | mx_name = d['name'] 590 | mx_origin = d['origin'] 591 | mx_focal_point = d['focal_point'] 592 | mx_up = d['up'] 593 | mx_focal_length = d['focal_length'] 594 | mx_sensor_fit = d['sensor_fit'] 595 | mx_film_width = d['film_width'] 596 | mx_film_height = d['film_height'] 597 | mx_xres = d['x_res'] 598 | mx_yres = d['y_res'] 599 | mx_active = d['active'] 600 | mx_zclip = d['zclip'] 601 | mx_zclip_near = d['zclip_near'] 602 | mx_zclip_far = d['zclip_far'] 603 | mx_shift_x = d['shift_x'] 604 | mx_shift_y = d['shift_y'] 605 | 606 | # convert axes 607 | cm = io_utils.axis_conversion(from_forward='-Y', to_forward='Z', from_up='Z', to_up='Y') 608 | cm.to_4x4() 609 | eye = Vector(mx_origin) * cm 610 | target = Vector(mx_focal_point) * cm 611 | up = Vector(mx_up) * cm 612 | 613 | cd = bpy.data.cameras.new(mx_name) 614 | c = bpy.data.objects.new(mx_name, cd) 615 | bpy.context.scene.objects.link(c) 616 | 617 | m = self._matrix_look_at(eye, target, up) 618 | c.matrix_world = m 619 | 620 | # distance 621 | mx_dof_distance = self._distance(mx_origin, mx_focal_point) 622 | 623 | # camera properties 624 | cd.lens = mx_focal_length 625 | cd.dof_distance = mx_dof_distance 626 | cd.sensor_fit = mx_sensor_fit 627 | cd.sensor_width = mx_film_width 628 | cd.sensor_height = mx_film_height 629 | 630 | cd.clip_start = mx_zclip_near 631 | cd.clip_end = mx_zclip_far 632 | cd.shift_x = mx_shift_x / 10.0 633 | cd.shift_y = mx_shift_y / 10.0 634 | 635 | if(mx_active): 636 | render = bpy.context.scene.render 637 | render.resolution_x = mx_xres 638 | render.resolution_y = mx_yres 639 | render.resolution_percentage = 100 640 | bpy.context.scene.camera = c 641 | 642 | return c 643 | 644 | def _sun(self, d): 645 | n = d['name'] 646 | log("sun: {0}".format(n), 2) 647 | l = bpy.data.lamps.new(n, 'SUN') 648 | o = utils.add_object2(n, l) 649 | v = Vector(d['xyz']) 650 | mrx90 = Matrix.Rotation(math.radians(90.0), 4, 'X') 651 | v.rotate(mrx90) 652 | m = self._matrix_look_at(v, Vector((0.0, 0.0, 0.0)), Vector((0.0, 0.0, 1.0))) 653 | o.matrix_world = m 654 | 655 | # align sun ray (which is 25bu long) end with scene center 656 | d = 25 657 | l, r, s = m.decompose() 658 | n = Vector((0.0, 0.0, 1.0)) 659 | n.rotate(r) 660 | loc = maths.shift_vert_along_normal(l, n, d - 1) 661 | o.location = loc 662 | 663 | return o 664 | 665 | def _hierarchy(self, data): 666 | types = ['MESH', 'INSTANCE', 'EMPTY'] 667 | for d in data: 668 | t = d['type'] 669 | if(t in types): 670 | # o = self._get_object_by_name(d['name']) 671 | o = d['created'] 672 | if(d['parent'] is not None): 673 | # p = self._get_object_by_name(d['parent']) 674 | p = None 675 | for q in data: 676 | if(q['name'] == d['parent']): 677 | p = q['created'] 678 | break 679 | o.parent = p 680 | 681 | def _transformations(self, data): 682 | types = ['MESH', 'INSTANCE', 'EMPTY'] 683 | mrx90 = Matrix.Rotation(math.radians(90.0), 4, 'X') 684 | for d in data: 685 | t = d['type'] 686 | if(t in types): 687 | # o = self._get_object_by_name(d['name']) 688 | o = d['created'] 689 | m = self._base_and_pivot_to_matrix(d) 690 | if(o.type == 'MESH'): 691 | if(d['type'] != 'INSTANCE'): 692 | o.data.transform(mrx90) 693 | o.matrix_local = m 694 | 695 | def _distance(self, a, b): 696 | ax, ay, az = a 697 | bx, by, bz = b 698 | return ((ax - bx) ** 2 + (ay - by) ** 2 + (az - bz) ** 2) ** 0.5 699 | 700 | def _base_and_pivot_to_matrix(self, d): 701 | b = d['base'] 702 | p = d['pivot'] 703 | o, x, y, z = b 704 | m = Matrix(((x[0], z[0] * -1, y[0], o[0]), 705 | (x[2] * -1, z[2], y[2] * -1, o[2] * -1), 706 | (x[1], z[1] * -1, y[1], o[1]), 707 | (0.0, 0.0, 0.0, 1.0), )) 708 | return m 709 | 710 | def _matrix_look_at(self, eye, target, up): 711 | # https://github.com/mono/opentk/blob/master/Source/OpenTK/Math/Matrix4.cs 712 | 713 | z = eye - target 714 | x = up.cross(z) 715 | y = z.cross(x) 716 | 717 | x.normalize() 718 | y.normalize() 719 | z.normalize() 720 | 721 | rot = Matrix() 722 | rot[0][0] = x[0] 723 | rot[0][1] = y[0] 724 | rot[0][2] = z[0] 725 | rot[0][3] = 0 726 | rot[1][0] = x[1] 727 | rot[1][1] = y[1] 728 | rot[1][2] = z[1] 729 | rot[1][3] = 0 730 | rot[2][0] = x[2] 731 | rot[2][1] = y[2] 732 | rot[2][2] = z[2] 733 | rot[2][3] = 0 734 | 735 | # eye not need to be minus cmp to opentk 736 | # perhaps opentk has z inverse axis 737 | tran = Matrix.Translation(eye) 738 | return tran * rot 739 | 740 | def _finalize(self): 741 | # maybe i am just setting normals badly? how to find out? 742 | 743 | # remove strange smooth shading >> cycle edit mode on all meshes.. 744 | # i have no idea why this happens, never happended before, but this seems to fix that. 745 | cycled_meshes = [] 746 | for o in bpy.data.objects: 747 | if(o.type == 'MESH'): 748 | # skip instances, apply only on first mesh multiuser encountered.. 749 | if(o.data in cycled_meshes): 750 | pass 751 | else: 752 | bpy.ops.object.select_all(action='DESELECT') 753 | o.select = True 754 | bpy.context.scene.objects.active = o 755 | bpy.ops.object.mode_set(mode='EDIT') 756 | bpy.ops.object.mode_set(mode='OBJECT') 757 | cycled_meshes.append(o.data) 758 | 759 | 760 | class MXMImportMacOSX(): 761 | def __init__(self, mxm_path, ): 762 | self.TEMPLATE = system.check_for_import_mxm_template() 763 | self.mxm_path = os.path.realpath(mxm_path) 764 | self._import() 765 | 766 | def _import(self): 767 | log("{0} {1} {0}".format("-" * 30, self.__class__.__name__), 0, LogStyles.MESSAGE, prefix="", ) 768 | 769 | self.uuid = uuid.uuid1() 770 | 771 | h, t = os.path.split(self.mxm_path) 772 | n, e = os.path.splitext(t) 773 | if(bpy.data.filepath == ""): 774 | p = os.path.join(h, '{}-tmp-import_material-{}'.format(n, self.uuid)) 775 | self.tmp_dir = utils.tmp_dir(override_path=p) 776 | else: 777 | self.tmp_dir = utils.tmp_dir(purpose='import_material', uid=self.uuid, use_blend_name=False, custom_name=n, ) 778 | 779 | self.data_name = "{0}-{1}.json".format(n, self.uuid) 780 | self.script_name = "{0}-{1}.py".format(n, self.uuid) 781 | self.data_path = os.path.join(self.tmp_dir, self.data_name) 782 | 783 | log("executing script..", 1, LogStyles.MESSAGE) 784 | self._pymaxwell() 785 | log("processing objects..", 1, LogStyles.MESSAGE) 786 | self._process() 787 | log("cleanup..", 1, LogStyles.MESSAGE) 788 | self._cleanup() 789 | log("done.", 1, LogStyles.MESSAGE) 790 | 791 | def _pymaxwell(self): 792 | # generate script 793 | self.script_path = os.path.join(self.tmp_dir, self.script_name) 794 | with open(self.script_path, mode='w', encoding='utf-8') as f: 795 | # read template 796 | with open(self.TEMPLATE, encoding='utf-8') as t: 797 | code = "".join(t.readlines()) 798 | # write template to a new file 799 | f.write(code) 800 | 801 | if(system.PLATFORM == 'Darwin'): 802 | system.python34_run_script_helper_import_mxm(self.script_path, self.mxm_path, self.data_path, ) 803 | elif(system.PLATFORM == 'Linux'): 804 | pass 805 | elif(system.PLATFORM == 'Windows'): 806 | pass 807 | else: 808 | pass 809 | 810 | def _process(self): 811 | data = None 812 | 813 | if(not os.path.exists(self.data_path)): 814 | raise RuntimeError("Protected MXS?") 815 | 816 | with open(self.data_path, 'r') as f: 817 | data = json.load(f) 818 | 819 | self.data = data 820 | 821 | def _cleanup(self): 822 | def rm(p): 823 | if(os.path.exists(p)): 824 | os.remove(p) 825 | else: 826 | log("_cleanup(): {} does not exist?".format(p), 1, LogStyles.WARNING, ) 827 | 828 | rm(self.script_path) 829 | rm(self.data_path) 830 | 831 | if(os.path.exists(self.tmp_dir)): 832 | os.rmdir(self.tmp_dir) 833 | else: 834 | log("_cleanup(): {} does not exist?".format(self.tmp_dir), 1, LogStyles.WARNING, ) 835 | 836 | 837 | class MXMImportWinLin(): 838 | def __init__(self, mxm_path, ): 839 | log("{0} {1} {0}".format("-" * 30, self.__class__.__name__), 0, LogStyles.MESSAGE, prefix="", ) 840 | log("path: {}".format(mxm_path), 1, LogStyles.MESSAGE) 841 | r = mxs.MXMReader(mxm_path) 842 | self.data = r.data 843 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # This program is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU General Public License 5 | # as published by the Free Software Foundation; either version 2 6 | # of the License, or (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU General Public License 14 | # along with this program; if not, write to the Free Software Foundation, 15 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 | # 17 | # ##### END GPL LICENSE BLOCK ##### 18 | 19 | import math 20 | import sys 21 | import os 22 | import time 23 | import datetime 24 | import uuid 25 | 26 | import bpy 27 | from mathutils import Matrix, Vector 28 | 29 | from .log import log, LogStyles 30 | 31 | 32 | def color_temperature_to_rgb(kelvins): 33 | # http://www.vendian.org/mncharity/dir3/blackbody/ 34 | # http://www.vendian.org/mncharity/dir3/blackbody/UnstableURLs/bbr_color.html 35 | # http://www.vendian.org/mncharity/dir3/blackbody/UnstableURLs/bbr_color.txt 36 | 37 | table = ((1000.0, (1.0, 0.0337, 0.0)), (1100.0, (1.0, 0.0592, 0.0)), (1200.0, (1.0, 0.0846, 0.0)), (1300.0, (1.0, 0.1096, 0.0)), (1400.0, (1.0, 0.1341, 0.0)), 38 | (1500.0, (1.0, 0.1578, 0.0)), (1600.0, (1.0, 0.1806, 0.0)), (1700.0, (1.0, 0.2025, 0.0)), (1800.0, (1.0, 0.2235, 0.0)), (1900.0, (1.0, 0.2434, 0.0)), 39 | (2000.0, (1.0, 0.2647, 0.0033)), (2100.0, (1.0, 0.2889, 0.012)), (2200.0, (1.0, 0.3126, 0.0219)), (2300.0, (1.0, 0.336, 0.0331)), (2400.0, (1.0, 0.3589, 0.0454)), 40 | (2500.0, (1.0, 0.3814, 0.0588)), (2600.0, (1.0, 0.4034, 0.0734)), (2700.0, (1.0, 0.425, 0.0889)), (2800.0, (1.0, 0.4461, 0.1054)), (2900.0, (1.0, 0.4668, 0.1229)), 41 | (3000.0, (1.0, 0.487, 0.1411)), (3100.0, (1.0, 0.5067, 0.1602)), (3200.0, (1.0, 0.5259, 0.18)), (3300.0, (1.0, 0.5447, 0.2005)), (3400.0, (1.0, 0.563, 0.2216)), 42 | (3500.0, (1.0, 0.5809, 0.2433)), (3600.0, (1.0, 0.5983, 0.2655)), (3700.0, (1.0, 0.6153, 0.2881)), (3800.0, (1.0, 0.6318, 0.3112)), (3900.0, (1.0, 0.648, 0.3346)), 43 | (4000.0, (1.0, 0.6636, 0.3583)), (4100.0, (1.0, 0.6789, 0.3823)), (4200.0, (1.0, 0.6938, 0.4066)), (4300.0, (1.0, 0.7083, 0.431)), (4400.0, (1.0, 0.7223, 0.4556)), 44 | (4500.0, (1.0, 0.736, 0.4803)), (4600.0, (1.0, 0.7494, 0.5051)), (4700.0, (1.0, 0.7623, 0.5299)), (4800.0, (1.0, 0.775, 0.5548)), (4900.0, (1.0, 0.7872, 0.5797)), 45 | (5000.0, (1.0, 0.7992, 0.6045)), (5100.0, (1.0, 0.8108, 0.6293)), (5200.0, (1.0, 0.8221, 0.6541)), (5300.0, (1.0, 0.833, 0.6787)), (5400.0, (1.0, 0.8437, 0.7032)), 46 | (5500.0, (1.0, 0.8541, 0.7277)), (5600.0, (1.0, 0.8642, 0.7519)), (5700.0, (1.0, 0.874, 0.776)), (5800.0, (1.0, 0.8836, 0.8)), (5900.0, (1.0, 0.8929, 0.8238)), 47 | (6000.0, (1.0, 0.9019, 0.8473)), (6100.0, (1.0, 0.9107, 0.8707)), (6200.0, (1.0, 0.9193, 0.8939)), (6300.0, (1.0, 0.9276, 0.9168)), (6400.0, (1.0, 0.9357, 0.9396)), 48 | (6500.0, (1.0, 0.9436, 0.9621)), (6600.0, (1.0, 0.9513, 0.9844)), (6700.0, (0.9937, 0.9526, 1.0)), (6800.0, (0.9726, 0.9395, 1.0)), (6900.0, (0.9526, 0.927, 1.0)), 49 | (7000.0, (0.9337, 0.915, 1.0)), (7100.0, (0.9157, 0.9035, 1.0)), (7200.0, (0.8986, 0.8925, 1.0)), (7300.0, (0.8823, 0.8819, 1.0)), (7400.0, (0.8668, 0.8718, 1.0)), 50 | (7500.0, (0.852, 0.8621, 1.0)), (7600.0, (0.8379, 0.8527, 1.0)), (7700.0, (0.8244, 0.8437, 1.0)), (7800.0, (0.8115, 0.8351, 1.0)), (7900.0, (0.7992, 0.8268, 1.0)), 51 | (8000.0, (0.7874, 0.8187, 1.0)), (8100.0, (0.7761, 0.811, 1.0)), (8200.0, (0.7652, 0.8035, 1.0)), (8300.0, (0.7548, 0.7963, 1.0)), (8400.0, (0.7449, 0.7894, 1.0)), 52 | (8500.0, (0.7353, 0.7827, 1.0)), (8600.0, (0.726, 0.7762, 1.0)), (8700.0, (0.7172, 0.7699, 1.0)), (8800.0, (0.7086, 0.7638, 1.0)), (8900.0, (0.7004, 0.7579, 1.0)), 53 | (9000.0, (0.6925, 0.7522, 1.0)), (9100.0, (0.6848, 0.7467, 1.0)), (9200.0, (0.6774, 0.7414, 1.0)), (9300.0, (0.6703, 0.7362, 1.0)), (9400.0, (0.6635, 0.7311, 1.0)), 54 | (9500.0, (0.6568, 0.7263, 1.0)), (9600.0, (0.6504, 0.7215, 1.0)), (9700.0, (0.6442, 0.7169, 1.0)), (9800.0, (0.6382, 0.7124, 1.0)), (9900.0, (0.6324, 0.7081, 1.0)), 55 | (10000.0, (0.6268, 0.7039, 1.0)), (10100.0, (0.6213, 0.6998, 1.0)), (10200.0, (0.6161, 0.6958, 1.0)), (10300.0, (0.6109, 0.6919, 1.0)), (10400.0, (0.606, 0.6881, 1.0)), 56 | (10500.0, (0.6012, 0.6844, 1.0)), (10600.0, (0.5965, 0.6808, 1.0)), (10700.0, (0.5919, 0.6773, 1.0)), (10800.0, (0.5875, 0.6739, 1.0)), (10900.0, (0.5833, 0.6706, 1.0)), 57 | (11000.0, (0.5791, 0.6674, 1.0)), (11100.0, (0.575, 0.6642, 1.0)), (11200.0, (0.5711, 0.6611, 1.0)), (11300.0, (0.5673, 0.6581, 1.0)), (11400.0, (0.5636, 0.6552, 1.0)), 58 | (11500.0, (0.5599, 0.6523, 1.0)), (11600.0, (0.5564, 0.6495, 1.0)), (11700.0, (0.553, 0.6468, 1.0)), (11800.0, (0.5496, 0.6441, 1.0)), (11900.0, (0.5463, 0.6415, 1.0)), 59 | (12000.0, (0.5431, 0.6389, 1.0)), (12100.0, (0.54, 0.6364, 1.0)), (12200.0, (0.537, 0.634, 1.0)), (12300.0, (0.534, 0.6316, 1.0)), (12400.0, (0.5312, 0.6293, 1.0)), 60 | (12500.0, (0.5283, 0.627, 1.0)), (12600.0, (0.5256, 0.6247, 1.0)), (12700.0, (0.5229, 0.6225, 1.0)), (12800.0, (0.5203, 0.6204, 1.0)), (12900.0, (0.5177, 0.6183, 1.0)), 61 | (13000.0, (0.5152, 0.6162, 1.0)), (13100.0, (0.5128, 0.6142, 1.0)), (13200.0, (0.5104, 0.6122, 1.0)), (13300.0, (0.508, 0.6103, 1.0)), (13400.0, (0.5057, 0.6084, 1.0)), 62 | (13500.0, (0.5035, 0.6065, 1.0)), (13600.0, (0.5013, 0.6047, 1.0)), (13700.0, (0.4991, 0.6029, 1.0)), (13800.0, (0.497, 0.6012, 1.0)), (13900.0, (0.495, 0.5994, 1.0)), 63 | (14000.0, (0.493, 0.5978, 1.0)), (14100.0, (0.491, 0.5961, 1.0)), (14200.0, (0.4891, 0.5945, 1.0)), (14300.0, (0.4872, 0.5929, 1.0)), (14400.0, (0.4853, 0.5913, 1.0)), 64 | (14500.0, (0.4835, 0.5898, 1.0)), (14600.0, (0.4817, 0.5882, 1.0)), (14700.0, (0.4799, 0.5868, 1.0)), (14800.0, (0.4782, 0.5853, 1.0)), (14900.0, (0.4765, 0.5839, 1.0)), 65 | (15000.0, (0.4749, 0.5824, 1.0)), (15100.0, (0.4733, 0.5811, 1.0)), (15200.0, (0.4717, 0.5797, 1.0)), (15300.0, (0.4701, 0.5784, 1.0)), (15400.0, (0.4686, 0.577, 1.0)), 66 | (15500.0, (0.4671, 0.5757, 1.0)), (15600.0, (0.4656, 0.5745, 1.0)), (15700.0, (0.4641, 0.5732, 1.0)), (15800.0, (0.4627, 0.572, 1.0)), (15900.0, (0.4613, 0.5708, 1.0)), 67 | (16000.0, (0.4599, 0.5696, 1.0)), (16100.0, (0.4586, 0.5684, 1.0)), (16200.0, (0.4572, 0.5673, 1.0)), (16300.0, (0.4559, 0.5661, 1.0)), (16400.0, (0.4546, 0.565, 1.0)), 68 | (16500.0, (0.4534, 0.5639, 1.0)), (16600.0, (0.4521, 0.5628, 1.0)), (16700.0, (0.4509, 0.5617, 1.0)), (16800.0, (0.4497, 0.5607, 1.0)), (16900.0, (0.4485, 0.5597, 1.0)), 69 | (17000.0, (0.4474, 0.5586, 1.0)), (17100.0, (0.4462, 0.5576, 1.0)), (17200.0, (0.4451, 0.5566, 1.0)), (17300.0, (0.444, 0.5557, 1.0)), (17400.0, (0.4429, 0.5547, 1.0)), 70 | (17500.0, (0.4418, 0.5538, 1.0)), (17600.0, (0.4408, 0.5528, 1.0)), (17700.0, (0.4397, 0.5519, 1.0)), (17800.0, (0.4387, 0.551, 1.0)), (17900.0, (0.4377, 0.5501, 1.0)), 71 | (18000.0, (0.4367, 0.5492, 1.0)), (18100.0, (0.4357, 0.5483, 1.0)), (18200.0, (0.4348, 0.5475, 1.0)), (18300.0, (0.4338, 0.5466, 1.0)), (18400.0, (0.4329, 0.5458, 1.0)), 72 | (18500.0, (0.4319, 0.545, 1.0)), (18600.0, (0.431, 0.5442, 1.0)), (18700.0, (0.4301, 0.5434, 1.0)), (18800.0, (0.4293, 0.5426, 1.0)), (18900.0, (0.4284, 0.5418, 1.0)), 73 | (19000.0, (0.4275, 0.541, 1.0)), (19100.0, (0.4267, 0.5403, 1.0)), (19200.0, (0.4258, 0.5395, 1.0)), (19300.0, (0.425, 0.5388, 1.0)), (19400.0, (0.4242, 0.5381, 1.0)), 74 | (19500.0, (0.4234, 0.5373, 1.0)), (19600.0, (0.4226, 0.5366, 1.0)), (19700.0, (0.4218, 0.5359, 1.0)), (19800.0, (0.4211, 0.5352, 1.0)), (19900.0, (0.4203, 0.5345, 1.0)), 75 | (20000.0, (0.4196, 0.5339, 1.0)), (20100.0, (0.4188, 0.5332, 1.0)), (20200.0, (0.4181, 0.5325, 1.0)), (20300.0, (0.4174, 0.5319, 1.0)), (20400.0, (0.4167, 0.5312, 1.0)), 76 | (20500.0, (0.416, 0.5306, 1.0)), (20600.0, (0.4153, 0.53, 1.0)), (20700.0, (0.4146, 0.5293, 1.0)), (20800.0, (0.4139, 0.5287, 1.0)), (20900.0, (0.4133, 0.5281, 1.0)), 77 | (21000.0, (0.4126, 0.5275, 1.0)), (21100.0, (0.4119, 0.5269, 1.0)), (21200.0, (0.4113, 0.5264, 1.0)), (21300.0, (0.4107, 0.5258, 1.0)), (21400.0, (0.41, 0.5252, 1.0)), 78 | (21500.0, (0.4094, 0.5246, 1.0)), (21600.0, (0.4088, 0.5241, 1.0)), (21700.0, (0.4082, 0.5235, 1.0)), (21800.0, (0.4076, 0.523, 1.0)), (21900.0, (0.407, 0.5224, 1.0)), 79 | (22000.0, (0.4064, 0.5219, 1.0)), (22100.0, (0.4059, 0.5214, 1.0)), (22200.0, (0.4053, 0.5209, 1.0)), (22300.0, (0.4047, 0.5203, 1.0)), (22400.0, (0.4042, 0.5198, 1.0)), 80 | (22500.0, (0.4036, 0.5193, 1.0)), (22600.0, (0.4031, 0.5188, 1.0)), (22700.0, (0.4026, 0.5183, 1.0)), (22800.0, (0.402, 0.5178, 1.0)), (22900.0, (0.4015, 0.5174, 1.0)), 81 | (23000.0, (0.401, 0.5169, 1.0)), (23100.0, (0.4005, 0.5164, 1.0)), (23200.0, (0.4, 0.5159, 1.0)), (23300.0, (0.3995, 0.5155, 1.0)), (23400.0, (0.399, 0.515, 1.0)), 82 | (23500.0, (0.3985, 0.5146, 1.0)), (23600.0, (0.398, 0.5141, 1.0)), (23700.0, (0.3975, 0.5137, 1.0)), (23800.0, (0.397, 0.5132, 1.0)), (23900.0, (0.3966, 0.5128, 1.0)), 83 | (24000.0, (0.3961, 0.5123, 1.0)), (24100.0, (0.3956, 0.5119, 1.0)), (24200.0, (0.3952, 0.5115, 1.0)), (24300.0, (0.3947, 0.5111, 1.0)), (24400.0, (0.3943, 0.5107, 1.0)), 84 | (24500.0, (0.3938, 0.5103, 1.0)), (24600.0, (0.3934, 0.5098, 1.0)), (24700.0, (0.393, 0.5094, 1.0)), (24800.0, (0.3925, 0.509, 1.0)), (24900.0, (0.3921, 0.5086, 1.0)), 85 | (25000.0, (0.3917, 0.5083, 1.0)), (25100.0, (0.3913, 0.5079, 1.0)), (25200.0, (0.3909, 0.5075, 1.0)), (25300.0, (0.3905, 0.5071, 1.0)), (25400.0, (0.3901, 0.5067, 1.0)), 86 | (25500.0, (0.3897, 0.5064, 1.0)), (25600.0, (0.3893, 0.506, 1.0)), (25700.0, (0.3889, 0.5056, 1.0)), (25800.0, (0.3885, 0.5053, 1.0)), (25900.0, (0.3881, 0.5049, 1.0)), 87 | (26000.0, (0.3877, 0.5045, 1.0)), (26100.0, (0.3874, 0.5042, 1.0)), (26200.0, (0.387, 0.5038, 1.0)), (26300.0, (0.3866, 0.5035, 1.0)), (26400.0, (0.3863, 0.5032, 1.0)), 88 | (26500.0, (0.3859, 0.5028, 1.0)), (26600.0, (0.3855, 0.5025, 1.0)), (26700.0, (0.3852, 0.5021, 1.0)), (26800.0, (0.3848, 0.5018, 1.0)), (26900.0, (0.3845, 0.5015, 1.0)), 89 | (27000.0, (0.3841, 0.5012, 1.0)), (27100.0, (0.3838, 0.5008, 1.0)), (27200.0, (0.3835, 0.5005, 1.0)), (27300.0, (0.3831, 0.5002, 1.0)), (27400.0, (0.3828, 0.4999, 1.0)), 90 | (27500.0, (0.3825, 0.4996, 1.0)), (27600.0, (0.3821, 0.4993, 1.0)), (27700.0, (0.3818, 0.499, 1.0)), (27800.0, (0.3815, 0.4987, 1.0)), (27900.0, (0.3812, 0.4984, 1.0)), 91 | (28000.0, (0.3809, 0.4981, 1.0)), (28100.0, (0.3805, 0.4978, 1.0)), (28200.0, (0.3802, 0.4975, 1.0)), (28300.0, (0.3799, 0.4972, 1.0)), (28400.0, (0.3796, 0.4969, 1.0)), 92 | (28500.0, (0.3793, 0.4966, 1.0)), (28600.0, (0.379, 0.4963, 1.0)), (28700.0, (0.3787, 0.496, 1.0)), (28800.0, (0.3784, 0.4958, 1.0)), (28900.0, (0.3781, 0.4955, 1.0)), 93 | (29000.0, (0.3779, 0.4952, 1.0)), (29100.0, (0.3776, 0.4949, 1.0)), (29200.0, (0.3773, 0.4947, 1.0)), (29300.0, (0.377, 0.4944, 1.0)), (29400.0, (0.3767, 0.4941, 1.0)), 94 | (29500.0, (0.3764, 0.4939, 1.0)), (29600.0, (0.3762, 0.4936, 1.0)), (29700.0, (0.3759, 0.4934, 1.0)), (29800.0, (0.3756, 0.4931, 1.0)), (29900.0, (0.3754, 0.4928, 1.0)), 95 | (30000.0, (0.3751, 0.4926, 1.0)), (30100.0, (0.3748, 0.4923, 1.0)), (30200.0, (0.3746, 0.4921, 1.0)), (30300.0, (0.3743, 0.4918, 1.0)), (30400.0, (0.3741, 0.4916, 1.0)), 96 | (30500.0, (0.3738, 0.4914, 1.0)), (30600.0, (0.3735, 0.4911, 1.0)), (30700.0, (0.3733, 0.4909, 1.0)), (30800.0, (0.373, 0.4906, 1.0)), (30900.0, (0.3728, 0.4904, 1.0)), 97 | (31000.0, (0.3726, 0.4902, 1.0)), (31100.0, (0.3723, 0.4899, 1.0)), (31200.0, (0.3721, 0.4897, 1.0)), (31300.0, (0.3718, 0.4895, 1.0)), (31400.0, (0.3716, 0.4893, 1.0)), 98 | (31500.0, (0.3714, 0.489, 1.0)), (31600.0, (0.3711, 0.4888, 1.0)), (31700.0, (0.3709, 0.4886, 1.0)), (31800.0, (0.3707, 0.4884, 1.0)), (31900.0, (0.3704, 0.4881, 1.0)), 99 | (32000.0, (0.3702, 0.4879, 1.0)), (32100.0, (0.37, 0.4877, 1.0)), (32200.0, (0.3698, 0.4875, 1.0)), (32300.0, (0.3695, 0.4873, 1.0)), (32400.0, (0.3693, 0.4871, 1.0)), 100 | (32500.0, (0.3691, 0.4869, 1.0)), (32600.0, (0.3689, 0.4867, 1.0)), (32700.0, (0.3687, 0.4864, 1.0)), (32800.0, (0.3684, 0.4862, 1.0)), (32900.0, (0.3682, 0.486, 1.0)), 101 | (33000.0, (0.368, 0.4858, 1.0)), (33100.0, (0.3678, 0.4856, 1.0)), (33200.0, (0.3676, 0.4854, 1.0)), (33300.0, (0.3674, 0.4852, 1.0)), (33400.0, (0.3672, 0.485, 1.0)), 102 | (33500.0, (0.367, 0.4848, 1.0)), (33600.0, (0.3668, 0.4847, 1.0)), (33700.0, (0.3666, 0.4845, 1.0)), (33800.0, (0.3664, 0.4843, 1.0)), (33900.0, (0.3662, 0.4841, 1.0)), 103 | (34000.0, (0.366, 0.4839, 1.0)), (34100.0, (0.3658, 0.4837, 1.0)), (34200.0, (0.3656, 0.4835, 1.0)), (34300.0, (0.3654, 0.4833, 1.0)), (34400.0, (0.3652, 0.4831, 1.0)), 104 | (34500.0, (0.365, 0.483, 1.0)), (34600.0, (0.3649, 0.4828, 1.0)), (34700.0, (0.3647, 0.4826, 1.0)), (34800.0, (0.3645, 0.4824, 1.0)), (34900.0, (0.3643, 0.4822, 1.0)), 105 | (35000.0, (0.3641, 0.4821, 1.0)), (35100.0, (0.3639, 0.4819, 1.0)), (35200.0, (0.3638, 0.4817, 1.0)), (35300.0, (0.3636, 0.4815, 1.0)), (35400.0, (0.3634, 0.4814, 1.0)), 106 | (35500.0, (0.3632, 0.4812, 1.0)), (35600.0, (0.363, 0.481, 1.0)), (35700.0, (0.3629, 0.4809, 1.0)), (35800.0, (0.3627, 0.4807, 1.0)), (35900.0, (0.3625, 0.4805, 1.0)), 107 | (36000.0, (0.3624, 0.4804, 1.0)), (36100.0, (0.3622, 0.4802, 1.0)), (36200.0, (0.362, 0.48, 1.0)), (36300.0, (0.3619, 0.4799, 1.0)), (36400.0, (0.3617, 0.4797, 1.0)), 108 | (36500.0, (0.3615, 0.4796, 1.0)), (36600.0, (0.3614, 0.4794, 1.0)), (36700.0, (0.3612, 0.4792, 1.0)), (36800.0, (0.361, 0.4791, 1.0)), (36900.0, (0.3609, 0.4789, 1.0)), 109 | (37000.0, (0.3607, 0.4788, 1.0)), (37100.0, (0.3605, 0.4786, 1.0)), (37200.0, (0.3604, 0.4785, 1.0)), (37300.0, (0.3602, 0.4783, 1.0)), (37400.0, (0.3601, 0.4782, 1.0)), 110 | (37500.0, (0.3599, 0.478, 1.0)), (37600.0, (0.3598, 0.4779, 1.0)), (37700.0, (0.3596, 0.4777, 1.0)), (37800.0, (0.3595, 0.4776, 1.0)), (37900.0, (0.3593, 0.4774, 1.0)), 111 | (38000.0, (0.3592, 0.4773, 1.0)), (38100.0, (0.359, 0.4771, 1.0)), (38200.0, (0.3589, 0.477, 1.0)), (38300.0, (0.3587, 0.4768, 1.0)), (38400.0, (0.3586, 0.4767, 1.0)), 112 | (38500.0, (0.3584, 0.4766, 1.0)), (38600.0, (0.3583, 0.4764, 1.0)), (38700.0, (0.3581, 0.4763, 1.0)), (38800.0, (0.358, 0.4761, 1.0)), (38900.0, (0.3579, 0.476, 1.0)), 113 | (39000.0, (0.3577, 0.4759, 1.0)), (39100.0, (0.3576, 0.4757, 1.0)), (39200.0, (0.3574, 0.4756, 1.0)), (39300.0, (0.3573, 0.4755, 1.0)), (39400.0, (0.3572, 0.4753, 1.0)), 114 | (39500.0, (0.357, 0.4752, 1.0)), (39600.0, (0.3569, 0.4751, 1.0)), (39700.0, (0.3567, 0.4749, 1.0)), (39800.0, (0.3566, 0.4748, 1.0)), (39900.0, (0.3565, 0.4747, 1.0)), 115 | (40000.0, (0.3563, 0.4745, 1.0)), ) 116 | 117 | if(kelvins <= 1000.0): 118 | return table[0][1] 119 | elif(kelvins >= 40000.0): 120 | return table[len(table) - 1][1] 121 | else: 122 | r = round(kelvins / 100) * 100 123 | i = int(r / 100) - 10 124 | return table[i][1] 125 | 126 | 127 | def get_addon_bl_info(): 128 | a = os.path.split(os.path.split(os.path.realpath(__file__))[0])[1] 129 | m = sys.modules[a] 130 | return m.bl_info 131 | 132 | 133 | def get_plugin_id(): 134 | bli = get_addon_bl_info() 135 | n = bli.get('name', "") 136 | d = bli.get('description', "") 137 | v = bli.get('version', (0, 0, 0, )) 138 | v = ".".join([str(i) for i in v]) 139 | r = "" 140 | if(n != ""): 141 | r = "{} ({}), version: {}".format(n, d, v) 142 | return r 143 | 144 | 145 | def add_object(name, data): 146 | """Fastest way of adding new objects. 147 | All existing objects gets deselected, then new one is added, selected and made active. 148 | 149 | name - Name of the new object 150 | data - Data of the new object, Empty objects can be added by passing None. 151 | Returns newly created object. 152 | 153 | About 40% faster than object_utils.object_data_add and with Empty support. 154 | """ 155 | 156 | so = bpy.context.scene.objects 157 | for i in so: 158 | i.select = False 159 | o = bpy.data.objects.new(name, data) 160 | so.link(o) 161 | o.select = True 162 | if(so.active is None or so.active.mode == 'OBJECT'): 163 | so.active = o 164 | return o 165 | 166 | 167 | def add_object2(name, data, ): 168 | # skip select / active part.. 169 | o = bpy.data.objects.new(name, data, ) 170 | s = bpy.context.scene 171 | s.objects.link(o) 172 | return o 173 | 174 | 175 | def wipe_out_object(ob, and_data=True): 176 | # http://blenderartists.org/forum/showthread.php?222667-Remove-object-and-clear-from-memory&p=1888116&viewfull=1#post1888116 177 | 178 | data = bpy.data.objects[ob.name].data 179 | # never wipe data before unlink the ex-user object of the scene else crash (2.58 3 770 2) 180 | # so if there's more than one user for this data, never wipeOutData. will be done with the last user 181 | # if in the list 182 | if(data is not None): 183 | # change: if wiping empty, data in None and following will raise exception 184 | if(data.users > 1): 185 | and_data = False 186 | else: 187 | and_data = False 188 | 189 | # odd: 190 | ob = bpy.data.objects[ob.name] 191 | # if the ob (board) argument comes from bpy.data.groups['aGroup'].objects, 192 | # bpy.data.groups['board'].objects['board'].users_scene 193 | 194 | for sc in ob.users_scene: 195 | sc.objects.unlink(ob) 196 | 197 | try: 198 | bpy.data.objects.remove(ob) 199 | except: 200 | log('data.objects.remove issue with %s' % ob.name, style=LogStyles.ERROR, ) 201 | 202 | # never wipe data before unlink the ex-user object of the scene else crash (2.58 3 770 2) 203 | if(and_data): 204 | wipe_out_data(data) 205 | 206 | 207 | def wipe_out_data(data): 208 | # http://blenderartists.org/forum/showthread.php?222667-Remove-object-and-clear-from-memory&p=1888116&viewfull=1#post1888116 209 | 210 | if(data.users == 0): 211 | try: 212 | data.user_clear() 213 | if type(data) == bpy.types.Mesh: 214 | bpy.data.meshes.remove(data) 215 | elif type(data) == bpy.types.PointLamp: 216 | bpy.data.lamps.remove(data) 217 | elif type(data) == bpy.types.Camera: 218 | bpy.data.cameras.remove(data) 219 | elif type(data) in [bpy.types.Curve, bpy.types.TextCurve]: 220 | bpy.data.curves.remove(data) 221 | elif type(data) == bpy.types.MetaBall: 222 | bpy.data.metaballs.remove(data) 223 | elif type(data) == bpy.types.Lattice: 224 | bpy.data.lattices.remove(data) 225 | elif type(data) == bpy.types.Armature: 226 | bpy.data.armatures.remove(data) 227 | else: 228 | log('data still here : forgot %s' % type(data), style=LogStyles.ERROR, ) 229 | except: 230 | log('%s has no user_clear attribute.' % data.name, style=LogStyles.ERROR, ) 231 | else: 232 | log('%s has %s user(s) !' % (data.name, data.users), style=LogStyles.ERROR, ) 233 | 234 | 235 | class InstanceMeshGenerator(): 236 | """base instance mesh generator class""" 237 | def __init__(self): 238 | self.def_verts, self.def_edges, self.def_faces = self.generate() 239 | self.exponent_v = len(self.def_verts) 240 | self.exponent_e = len(self.def_edges) 241 | self.exponent_f = len(self.def_faces) 242 | 243 | def generate(self): 244 | dv = [] 245 | de = [] 246 | df = [] 247 | # ------------------------------------------- 248 | 249 | dv.append((0, 0, 0)) 250 | 251 | # ------------------------------------------- 252 | return dv, de, df 253 | 254 | 255 | class CylinderMeshGenerator(InstanceMeshGenerator): 256 | def __init__(self, height=1, radius=0.5, sides=6, z_translation=0, z_rotation=0, z_scale=1, ngon_fill=True, inverse=False, enhanced=False, hed=-1, ): 257 | if(height <= 0): 258 | log("height is (or less than) 0, which is ridiculous. setting to 0.001..", 1) 259 | height = 0.001 260 | self.height = height 261 | 262 | if(radius <= 0): 263 | log("radius is (or less than) 0, which is ridiculous. setting to 0.001..", 1) 264 | radius = 0.001 265 | self.radius = radius 266 | 267 | if(sides < 3): 268 | log("sides are less than 3 which is ridiculous. setting to 3..", 1) 269 | sides = 3 270 | self.sides = sides 271 | 272 | self.z_translation = z_translation 273 | self.z_rotation = z_rotation 274 | if(z_scale <= 0): 275 | log("z scale is (or less than) 0, which is ridiculous. setting to 0.001..", 1) 276 | z_scale = 0.001 277 | self.z_scale = z_scale 278 | 279 | self.ngon_fill = ngon_fill 280 | self.inverse = inverse 281 | 282 | self.enhanced = enhanced 283 | if(self.enhanced): 284 | if(self.radius < self.height): 285 | if(hed == -1): 286 | # default 287 | hed = self.radius / 8 288 | elif(hed <= 0): 289 | log("got ridiculous holding edge distance (smaller or equal to 0).. setting to radius/8", 1) 290 | hed = self.radius / 8 291 | elif(hed >= self.radius): 292 | log("got ridiculous holding edge distance (higher or equal to radius).. setting to radius/8", 1) 293 | hed = self.radius / 8 294 | else: 295 | if(hed == -1): 296 | # default 297 | hed = self.height / 8 298 | elif(hed <= 0): 299 | log("got ridiculous holding edge distance (smaller or equal to 0).. setting to height/8", 1) 300 | hed = self.height / 8 301 | elif(hed >= self.height): 302 | log("got ridiculous holding edge distance (higher or equal to height).. setting to height/8", 1) 303 | hed = self.height / 8 304 | self.hed = hed 305 | 306 | super(CylinderMeshGenerator, self).__init__() 307 | 308 | def generate(self): 309 | dv = [] 310 | de = [] 311 | df = [] 312 | # ------------------------------------------- 313 | 314 | zt = Matrix.Translation(Vector((0, 0, self.z_translation))) 315 | zr = Matrix.Rotation(math.radians(self.z_rotation), 4, (0.0, 0.0, 1.0)) 316 | zs = Matrix.Scale(self.z_scale, 4, (0.0, 0.0, 1.0)) 317 | inv = 0 318 | if(self.inverse): 319 | inv = 180 320 | ri = Matrix.Rotation(math.radians(inv), 4, (0.0, 1.0, 0.0)) 321 | mat = zt * zr * zs * ri 322 | 323 | def circle2d_coords(radius, steps, offset, ox, oy): 324 | r = [] 325 | angstep = 2 * math.pi / steps 326 | for i in range(steps): 327 | x = math.sin(i * angstep + offset) * radius + ox 328 | y = math.cos(i * angstep + offset) * radius + oy 329 | r.append((x, y)) 330 | return r 331 | 332 | def do_quads(o, s, q, cw): 333 | r = [] 334 | for i in range(s): 335 | a = o + ((s * q) + i) 336 | b = o + ((s * q) + i + 1) 337 | if(b == o + ((s * q) + s)): 338 | b = o + (s * q) 339 | c = o + ((s * q) + i + s) 340 | d = o + ((s * q) + i + s + 1) 341 | if(d == o + ((s * q) + s + s)): 342 | d = o + ((s * q) + s) 343 | # debug 344 | # print(a, b, c, d) 345 | # production 346 | # print(a, c, d, b) 347 | # if(cw): 348 | # df.append((a, b, d, c)) 349 | # else: 350 | # df.append((a, c, d, b)) 351 | 352 | if(cw): 353 | r.append((a, b, d, c)) 354 | else: 355 | r.append((a, c, d, b)) 356 | return r 357 | 358 | def do_tris(o, s, c, cw): 359 | r = [] 360 | for i in range(s): 361 | a = o + i 362 | b = o + i + 1 363 | if(b == o + s): 364 | b = o 365 | # debug 366 | # print(a, b, c) 367 | # production 368 | # print(b, a, c) 369 | # if(cw): 370 | # df.append((a, b, c)) 371 | # else: 372 | # df.append((b, a, c)) 373 | if(cw): 374 | r.append((a, b, c)) 375 | else: 376 | r.append((b, a, c)) 377 | return r 378 | 379 | l = self.height 380 | r = self.radius 381 | s = self.sides 382 | h = self.hed 383 | z = 0.0 384 | 385 | if(self.enhanced): 386 | cvv = [] 387 | 388 | # holding edge 389 | c = circle2d_coords(r - h, s, 0, 0, 0) 390 | for i in c: 391 | cvv.append(Vector((i[0], i[1], z))) 392 | 393 | # bottom circle 394 | c = circle2d_coords(r, s, 0, 0, 0) 395 | for i in c: 396 | cvv.append(Vector((i[0], i[1], z))) 397 | 398 | # holding edge 399 | c = circle2d_coords(r, s, 0, 0, 0) 400 | for i in c: 401 | cvv.append(Vector((i[0], i[1], h))) 402 | 403 | # holding edge 404 | c = circle2d_coords(r, s, 0, 0, 0) 405 | for i in c: 406 | cvv.append(Vector((i[0], i[1], l - h))) 407 | 408 | # bottom circle 409 | c = circle2d_coords(r, s, 0, 0, 0) 410 | for i in c: 411 | cvv.append(Vector((i[0], i[1], l))) 412 | 413 | # holding edge 414 | c = circle2d_coords(r - h, s, 0, 0, 0) 415 | for i in c: 416 | cvv.append(Vector((i[0], i[1], l))) 417 | 418 | if(self.ngon_fill is False): 419 | # bottom nad top center vertex 420 | cvv.append(Vector((z, z, z))) 421 | cvv.append(Vector((z, z, l))) 422 | 423 | for i, v in enumerate(cvv): 424 | # cvv[i] = v * mat 425 | cvv[i] = mat * v 426 | for v in cvv: 427 | dv.append(v.to_tuple()) 428 | 429 | qr = 5 430 | for q in range(qr): 431 | df.extend(do_quads(0, s, q, False)) 432 | 433 | if(self.ngon_fill): 434 | ng = [] 435 | for i in range(s): 436 | ng.append(i) 437 | df.append(tuple(ng)) 438 | ng = [] 439 | for i in range(len(dv) - s, len(dv)): 440 | ng.append(i) 441 | df.append(tuple(reversed(ng))) 442 | 443 | else: 444 | df.extend(do_tris(0, s, len(dv) - 2, True)) 445 | df.extend(do_tris(len(dv) - 2 - s, s, len(dv) - 1, False)) 446 | 447 | else: 448 | cvv = [] 449 | c = circle2d_coords(r, s, 0, 0, 0) 450 | for i in c: 451 | cvv.append(Vector((i[0], i[1], z))) 452 | c = circle2d_coords(r, s, 0, 0, 0) 453 | for i in c: 454 | cvv.append(Vector((i[0], i[1], l))) 455 | 456 | if(self.ngon_fill is False): 457 | cvv.append(Vector((0, 0, 0))) 458 | cvv.append(Vector((0, 0, l))) 459 | 460 | for i, v in enumerate(cvv): 461 | # cvv[i] = v * mat 462 | cvv[i] = mat * v 463 | for v in cvv: 464 | dv.append(v.to_tuple()) 465 | 466 | if(self.ngon_fill): 467 | df.extend(do_quads(0, s, 0, False)) 468 | 469 | ng = [] 470 | for i in range(s): 471 | ng.append(i) 472 | df.append(tuple(ng)) 473 | ng = [] 474 | for i in range(len(dv) - s, len(dv)): 475 | ng.append(i) 476 | df.append(tuple(reversed(ng))) 477 | 478 | else: 479 | df.extend(do_quads(0, s, 0, False)) 480 | df.extend(do_tris(0, s, len(dv) - 2, True)) 481 | df.extend(do_tris(len(dv) - 2 - s, s, len(dv) - 1, False)) 482 | 483 | # ------------------------------------------- 484 | return dv, de, df 485 | 486 | 487 | def benchmark(t=None): 488 | if(t is None): 489 | return time.time() 490 | d = datetime.timedelta(seconds=time.time() - t) 491 | log("benchmark: {} {}".format(d, '-' * 50), 0, LogStyles.MESSAGE) 492 | return time.time() 493 | 494 | 495 | def tmp_dir(purpose=None, uid=None, use_blend_name=False, custom_name=None, override_path=None, ): 496 | """create temp directory, look into preferences where to create it, build its name, create and return its absolute path 497 | naming pattern: 'tmp-PURPOSE-UUID', without purpose 'tmp-UUID', when use_blend_name is True, 'BLEND_NAME-tmp_dir-PURPOSE-UUID' or 'BLEND_NAME-tmp_dir-UUID' 498 | purpose: string, something descriptive, such as 'material_preview' or so 499 | uid: to prevent overwriting what is already there, add uuid to directory name. probability that someone is using such names and probability for the identical name is sufficiently infinitesimal i guess 500 | use_blend_name: blend name as directory prefix 501 | custom_name: the same like use_blend_name but you can set whatever you want, use_blend_name should be False 502 | override_path: path is set outside, so use this one. only create if needed.. 503 | return absolute path string 504 | """ 505 | def prefs(): 506 | a = os.path.split(os.path.split(os.path.realpath(__file__))[0])[1] 507 | p = bpy.context.user_preferences.addons[a].preferences 508 | return p 509 | 510 | if(override_path is not None): 511 | if(os.path.exists(override_path) is False): 512 | os.makedirs(override_path) 513 | return override_path 514 | 515 | blend = os.path.realpath(bpy.path.abspath(bpy.data.filepath)) 516 | _, blend_file = os.path.split(blend) 517 | blend_name, _ = os.path.splitext(blend_file) 518 | 519 | if(purpose is None): 520 | purpose = "" 521 | 522 | if(uid is None or uid == ""): 523 | uid = uuid.uuid1() 524 | 525 | if(custom_name is None): 526 | custom_name = "" 527 | 528 | root = os.path.realpath(bpy.path.abspath("//")) 529 | if(prefs().tmp_dir_use == 'SPECIFIC_DIRECTORY'): 530 | tmpsd = os.path.realpath(bpy.path.abspath(prefs().tmp_dir_path)) 531 | if(os.path.exists(tmpsd)): 532 | if(os.path.isdir(tmpsd)): 533 | if(os.access(tmpsd, os.W_OK)): 534 | root = tmpsd 535 | else: 536 | log("specific temp directory ('{}') is not writeable, using default".format(tmpsd), 1, LogStyles.WARNING) 537 | else: 538 | log("specific temp directory ('{}') is not a directory, using default".format(tmpsd), 1, LogStyles.WARNING) 539 | else: 540 | log("specific temp directory ('{}') does not exist, using default".format(tmpsd), 1, LogStyles.WARNING) 541 | else: 542 | # tmp_dir_use == 'BLEND_DIRECTORY' ie '//' 543 | pass 544 | 545 | if(purpose == ""): 546 | if(use_blend_name): 547 | # BLEND_NAME-tmp-UUID 548 | tmp_dir = os.path.join(root, "{}-tmp-{}".format(blend_name, uid)) 549 | else: 550 | if(custom_name != ""): 551 | # CUSTOM_NAME-tmp-UUID 552 | tmp_dir = os.path.join(root, "{}-tmp-{}".format(custom_name, uid)) 553 | else: 554 | # tmp-UUID 555 | tmp_dir = os.path.join(root, "tmp-{}".format(uid)) 556 | else: 557 | if(use_blend_name): 558 | # BLEND_NAME-tmp-PURPOSE-UUID 559 | tmp_dir = os.path.join(root, "{}-tmp-{}-{}".format(blend_name, purpose, uid)) 560 | else: 561 | if(custom_name != ""): 562 | # CUSTOM_NAME-tmp-PURPOSE-UUID 563 | tmp_dir = os.path.join(root, "{}-tmp-{}-{}".format(custom_name, purpose, uid)) 564 | else: 565 | # tmp-PURPOSE-UUID 566 | tmp_dir = os.path.join(root, "tmp-{}-{}".format(purpose, uid)) 567 | 568 | if(os.path.exists(tmp_dir) is False): 569 | os.makedirs(tmp_dir) 570 | 571 | log("using temp directory at '{}'".format(tmp_dir), 1, LogStyles.MESSAGE, ) 572 | return tmp_dir 573 | --------------------------------------------------------------------------------