# SPDX-License-Identifier: GPL-2.0-or-later """Define the POV render engine from generic Blender RenderEngine class.""" import faulthandler faulthandler.enable() import bpy import builtins as __builtin__ import subprocess import os from sys import platform import time import re import tempfile from bpy.utils import register_class, unregister_class from . import render def console_get(context): #context = bpy.context for win in context.window_manager.windows: if win.screen is not None: scr = win.screen for area in scr.areas: if area.type == 'CONSOLE': for space in area.spaces: if space.type == 'CONSOLE': return area, space, win, scr return None, None, None, None def console_write(context, txt): area, space, window, screen = console_get() if space is None: return #context = bpy.context.copy() context.update(dict( area=area, space_data=space, region=area.regions[-1], window=window, screen=screen, )) for line in txt.split("\n"): bpy.ops.console.scrollback_append(context, text=line, type='INFO') """ class RENDER_OT_test(bpy.types.Operator): bl_idname = 'pov.oha_test' bl_label = 'Test' bl_options = {'REGISTER', 'UNDO'} txt: bpy.props.StringProperty( name='text', default='what?' ) def execute(self, context): try: console_write(context, self.txt) return {'FINISHED'} except: self.report({'INFO'}, 'Printing report to Info window.') return {'CANCELLED'} def console_print(*args, **kwargs): context = bpy.context #screens = (win.screen for win in context.window_manager.windows if win.screen is not None) for win in context.window_manager.windows: if win.screen is not None: scr = win.screen for a in scr.areas: if a.type == 'CONSOLE': try: c = {} c['area'] = a c['space_data'] = a.spaces.active c['region'] = a.regions[-1] c['window'] = win c['screen'] = scr s = " ".join([str(arg) for arg in args]) for line in s.split("\n"): bpy.ops.console.scrollback_append(c, text=line, type='INFO') except BaseException as e: print(e.__doc__) print('An exception occurred: {}'.format(e)) pass def print(*args, **kwargs): console_print(*args, **kwargs) # to Python Console __builtin__.print(*args, **kwargs) # to System Console """ user_dir = bpy.utils.resource_path('USER') preview_dir = os.path.join(user_dir, "preview") # Make sure Preview directory exists and is empty smoke_path = os.path.join(preview_dir, "smoke.df3") class PovRender(bpy.types.RenderEngine): """Define the external renderer""" bl_idname = 'POVRAY_RENDER' bl_label = "Persitence Of Vision" bl_use_eevee_viewport = True bl_use_shading_nodes_custom = False DELAY = 0.5 @staticmethod def _locate_binary(): """Identify POV engine""" addon_prefs = bpy.context.preferences.addons[__package__].preferences # Use the system preference if its set. if pov_binary:= addon_prefs.filepath_povray: if os.path.exists(pov_binary): return pov_binary # Implicit else, as here return was still not triggered: print("User Preferences path to povray %r NOT FOUND, checking $PATH" % pov_binary) # Windows Only # assume if there is a 64bit binary that the user has a 64bit capable OS if platform.startswith('win'): import winreg win_reg_key = winreg.OpenKey( winreg.HKEY_CURRENT_USER, "Software\\POV-Ray\\v3.7\\Windows" ) win_home = winreg.QueryValueEx(win_reg_key, "Home")[0] # First try 64bits UberPOV pov_binary = os.path.join(win_home, "bin", "uberpov64.exe") if os.path.exists(pov_binary): return pov_binary # Then try 64bits POV pov_binary = os.path.join(win_home, "bin", "pvengine64.exe") if os.path.exists(pov_binary): return pov_binary # search the path all os's pov_binary_default = "povray" os_path_ls = os.getenv("PATH").split(':') + [""] for dir_name in os_path_ls: pov_binary = os.path.join(dir_name, pov_binary_default) if os.path.exists(pov_binary): return pov_binary return "" def _export(self, depsgraph, pov_path, image_render_path): """gather all necessary output files paths user defined and auto generated and export there""" scene = bpy.context.scene if scene.pov.tempfiles_enable: self._temp_file_in = tempfile.NamedTemporaryFile(suffix=".pov", delete=False).name # PNG with POV 3.7, can show the background color with alpha. In the long run using the # POV-Ray interactive preview like bishop 3D could solve the preview for all formats. self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name self._temp_file_ini = tempfile.NamedTemporaryFile(suffix=".ini", delete=False).name log_path = os.path.join(tempfile.gettempdir(), "alltext.out") else: self._temp_file_in = pov_path + ".pov" # PNG with POV 3.7, can show the background color with alpha. In the long run using the # POV-Ray interactive preview like bishop 3D could solve the preview for all formats. self._temp_file_out = image_render_path + ".png" # self._temp_file_out = image_render_path + ".tga" self._temp_file_ini = pov_path + ".ini" scene_path = scene.pov.scene_path abs_log_path = bpy.path.abspath(scene_path) log_path= os.path.join(abs_log_path, "alltext.out") ''' self._temp_file_in = "/test.pov" # PNG with POV 3.7, can show the background color with alpha. In the long run using the # POV-Ray interactive preview like bishop 3D could solve the preview for all formats. self._temp_file_out = "/test.png" #self._temp_file_out = "/test.tga" self._temp_file_ini = "/test.ini" ''' self._temp_file_log = log_path # self._temp_file_log = log_path.replace('\\', '/') # unnecessary relying on os.path if scene.pov.text_block == "": def info_callback(txt): self.update_stats("", "POV-Ray 3.7: " + txt) # os.makedirs(user_dir, exist_ok=True) # handled with previews os.makedirs(preview_dir, exist_ok=True) render.write_pov(self._temp_file_in, scene, info_callback) else: pass def _render(self, depsgraph): """Export necessary files and render image.""" scene = bpy.context.scene try: os.remove(self._temp_file_out) # so as not to load the old file except OSError: pass pov_binary = PovRender._locate_binary() if not pov_binary: print("POV-Ray 3.7: could not execute povray, possibly POV-Ray isn't installed") return False render.write_pov_ini( self._temp_file_ini, self._temp_file_log, self._temp_file_in, self._temp_file_out ) print("***-STARTING-***") extra_args = [] # Always add user preferences include path field when specified if (pov_documents := bpy.context.preferences.addons[__package__].preferences.docpath_povray)!="": extra_args.append("+L"+ pov_documents) if scene.pov.command_line_switches != "": extra_args.extend(iter(scene.pov.command_line_switches.split(" "))) self._is_windows = False if platform.startswith('win'): self._is_windows = True if "/EXIT" not in extra_args and not scene.pov.pov_editor: extra_args.append("/EXIT") else: # added -d option to prevent render window popup which leads to segfault on linux extra_args.append("-d") # Start Rendering! try: self._process = subprocess.Popen( [pov_binary, self._temp_file_ini] + extra_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) except OSError: # TODO, report api print("POV-Ray 3.7: could not execute '%s'" % pov_binary) import traceback traceback.print_exc() print("***-DONE-***") return False else: print("Engine ready!...") print("Command line arguments passed: " + str(extra_args)) return True def _cleanup(self): """Delete temp files and unpacked ones""" for f in (self._temp_file_in, self._temp_file_ini, self._temp_file_out): for i in range(5): try: os.unlink(f) break except OSError: # Wait a bit before retrying file might be still in use by Blender, # and Windows does not know how to delete a file in use! time.sleep(self.DELAY) for i in render.unpacked_images: for j in range(5): try: os.unlink(i) break except OSError: # Wait a bit before retrying file might be still in use by Blender, # and Windows does not know how to delete a file in use! time.sleep(self.DELAY) # avoid some crashes if memory leaks from one render to the next? #self.free_blender_memory() def render(self, depsgraph): """Export necessary files from text editor and render image.""" scene = bpy.context.scene r = scene.render x = int(r.resolution_x * r.resolution_percentage * 0.01) y = int(r.resolution_y * r.resolution_percentage * 0.01) print("\n***INITIALIZING***") # This makes some tests on the render, returning True if all goes good, and False if # it was finished one way or the other. # It also pauses the script (time.sleep()) def _test_wait(): time.sleep(self.DELAY) # User interrupts the rendering if self.test_break(): try: self._process.terminate() print("***POV INTERRUPTED***") except OSError: pass return False try: poll_result = self._process.poll() except AttributeError: print("***CHECK POV PATH IN PREFERENCES***") return False # POV process is finisehd, one way or the other if poll_result is not None: if poll_result < 0: print("***POV PROCESS FAILED : %s ***" % poll_result) self.update_stats("", "POV-Ray 3.7: Failed") return False return True if bpy.context.scene.pov.text_block != "": if scene.pov.tempfiles_enable: self._temp_file_in = tempfile.NamedTemporaryFile(suffix=".pov", delete=False).name self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name # self._temp_file_out = tempfile.NamedTemporaryFile(suffix=".tga", delete=False).name self._temp_file_ini = tempfile.NamedTemporaryFile(suffix=".ini", delete=False).name self._temp_file_log = os.path.join(tempfile.gettempdir(), "alltext.out") else: pov_path = scene.pov.text_block image_render_path = os.path.splitext(pov_path)[0] self._temp_file_out = os.path.join(preview_dir, image_render_path) self._temp_file_in = os.path.join(preview_dir, pov_path) self._temp_file_ini = os.path.join( preview_dir, (os.path.splitext(self._temp_file_in)[0] + ".INI") ) self._temp_file_log = os.path.join(preview_dir, "alltext.out") ''' try: os.remove(self._temp_file_in) # so as not to load the old file except OSError: pass ''' print(scene.pov.text_block) text = bpy.data.texts[scene.pov.text_block] with open(self._temp_file_in, "w") as file: # Why are the newlines needed? file.write("\n") file.write(text.as_string()) file.write("\n") # has to be called to update the frame on exporting animations scene.frame_set(scene.frame_current) pov_binary = PovRender._locate_binary() if not pov_binary: print("Could not execute POV-Ray, which installation possibly isn't standard ?") return False # start ini UI options export self.update_stats("", "POV-Ray 3.7: Exporting ini options from Blender") render.write_pov_ini( self._temp_file_ini, self._temp_file_log, self._temp_file_in, self._temp_file_out, ) print("***-STARTING-***") extra_args = [] if scene.pov.command_line_switches != "": for new_arg in scene.pov.command_line_switches.split(" "): extra_args.append(new_arg) if platform.startswith('win'): if "/EXIT" not in extra_args and not scene.pov.pov_editor: extra_args.append("/EXIT") else: # added -d option to prevent render window popup which leads to segfault on linux extra_args.append("-d") # Start Rendering! try: if scene.pov.sdl_window_enable and not platform.startswith( 'win' ): # segfault on linux == False !!! env = {'POV_DISPLAY_SCALED': 'off'} env.update(os.environ) self._process = subprocess.Popen( [pov_binary, self._temp_file_ini], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, ) else: self._process = subprocess.Popen( [pov_binary, self._temp_file_ini] + extra_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) except OSError: # TODO, report api print("POV-Ray 3.7: could not execute '%s'" % pov_binary) import traceback traceback.print_exc() print("***-DONE-***") return False else: print("Engine ready!...") print("Command line arguments passed: " + str(extra_args)) # return True self.update_stats("", "POV-Ray 3.7: Parsing File") # Indented in main function now so repeated here but still not working # to bring back render result to its buffer if os.path.exists(self._temp_file_out): xmin = int(r.border_min_x * x) ymin = int(r.border_min_y * y) xmax = int(r.border_max_x * x) ymax = int(r.border_max_y * y) result = self.begin_result(0, 0, x, y) lay = result.layers[0] time.sleep(self.DELAY) try: lay.load_from_file(self._temp_file_out) except RuntimeError: print("***POV ERROR WHILE READING OUTPUT FILE***") self.end_result(result) # print(self._temp_file_log) #bring the pov log to blender console with proper path? with open( self._temp_file_log ) as f: # The with keyword automatically closes the file when you are done print(f.read()) # console_write(f.read()) self.update_stats("", "") if scene.pov.tempfiles_enable or scene.pov.deletefiles_enable: self._cleanup() else: # WIP output format # if r.image_settings.file_format == 'OPENEXR': # fformat = 'EXR' # render.image_settings.color_mode = 'RGBA' # else: # fformat = 'TGA' # r.image_settings.file_format = 'TARGA' # r.image_settings.color_mode = 'RGBA' blend_scene_name = bpy.data.filepath.split(os.path.sep)[-1].split(".")[0] pov_scene_name = "" pov_path = "" image_render_path = "" # has to be called to update the frame on exporting animations scene.frame_set(scene.frame_current) if not scene.pov.tempfiles_enable: # check paths pov_path = bpy.path.abspath(scene.pov.scene_path).replace('\\', '/') if pov_path == "": if bpy.data.is_saved: pov_path = bpy.path.abspath("//") else: pov_path = tempfile.gettempdir() elif pov_path.endswith("/"): if pov_path == "/": pov_path = bpy.path.abspath("//") else: pov_path = bpy.path.abspath(scene.pov.scene_path) if not os.path.exists(pov_path): try: os.makedirs(pov_path) except BaseException as e: print(e.__doc__) print('An exception occurred: {}'.format(e)) import traceback traceback.print_exc() print("POV-Ray 3.7: Cannot create scenes directory: %r" % pov_path) self.update_stats( "", "POV-Ray 3.7: Cannot create scenes directory %r" % pov_path ) time.sleep(2.0) # return ''' # Bug in POV-Ray RC3 image_render_path = bpy.path.abspath(scene.pov.renderimage_path).replace('\\','/') if image_render_path == "": if bpy.data.is_saved: image_render_path = bpy.path.abspath("//") else: image_render_path = tempfile.gettempdir() #print("Path: " + image_render_path) elif path.endswith("/"): if image_render_path == "/": image_render_path = bpy.path.abspath("//") else: image_render_path = bpy.path.abspath(scene.pov.) if not os.path.exists(path): print("POV-Ray 3.7: Cannot find render image directory") self.update_stats("", "POV-Ray 3.7: Cannot find render image directory") time.sleep(2.0) return ''' # check name if scene.pov.scene_name == "": if blend_scene_name != "": pov_scene_name = blend_scene_name else: pov_scene_name = "untitled" else: pov_scene_name = scene.pov.scene_name if os.path.isfile(pov_scene_name): pov_scene_name = os.path.basename(pov_scene_name) pov_scene_name = pov_scene_name.split('/')[-1].split('\\')[-1] if not pov_scene_name: print("POV-Ray 3.7: Invalid scene name") self.update_stats("", "POV-Ray 3.7: Invalid scene name") time.sleep(2.0) # return pov_scene_name = os.path.splitext(pov_scene_name)[0] print("Scene name: " + pov_scene_name) print("Export path: " + pov_path) pov_path = os.path.join(pov_path, pov_scene_name) pov_path = os.path.realpath(pov_path) image_render_path = pov_path # print("Render Image path: " + image_render_path) # start export self.update_stats("", "POV-Ray 3.7: Exporting data from Blender") self._export(depsgraph, pov_path, image_render_path) self.update_stats("", "POV-Ray 3.7: Parsing File") if not self._render(depsgraph): self.update_stats("", "POV-Ray 3.7: Not found") # return # r = scene.render # compute resolution # x = int(r.resolution_x * r.resolution_percentage * 0.01) # y = int(r.resolution_y * r.resolution_percentage * 0.01) # Wait for the file to be created # XXX This is no more valid, as 3.7 always creates output file once render is finished! parsing = re.compile(br"= \[Parsing\.\.\.\] =") rendering = re.compile(br"= \[Rendering\.\.\.\] =") percent = re.compile(r"\(([0-9]{1,3})%\)") # print("***POV WAITING FOR FILE***") data = b"" last_line = "" while _test_wait(): # POV in Windows did not output its stdout/stderr, it displayed them in its GUI # But now writes file if self._is_windows: self.update_stats("", "POV-Ray 3.7: Rendering File") else: t_data = self._process.stdout.read(10000) if not t_data: continue data += t_data # XXX This is working for UNIX, not sure whether it might need adjustments for # other OSs # First replace is for windows t_data = str(t_data).replace('\\r\\n', '\\n').replace('\\r', '\r') lines = t_data.split('\\n') last_line += lines[0] lines[0] = last_line print('\n'.join(lines), end="") last_line = lines[-1] if rendering.search(data): _pov_rendering = True match = percent.findall(str(data)) if match: self.update_stats("", "POV-Ray 3.7: Rendering File (%s%%)" % match[-1]) else: self.update_stats("", "POV-Ray 3.7: Rendering File") elif parsing.search(data): self.update_stats("", "POV-Ray 3.7: Parsing File") if os.path.exists(self._temp_file_out): # print("***POV FILE OK***") # self.update_stats("", "POV-Ray 3.7: Rendering") # prev_size = -1 xmin = int(r.border_min_x * x) ymin = int(r.border_min_y * y) xmax = int(r.border_max_x * x) ymax = int(r.border_max_y * y) # print("***POV UPDATING IMAGE***") result = self.begin_result(0, 0, x, y) # XXX, tests for border render. # result = self.begin_result(xmin, ymin, xmax - xmin, ymax - ymin) # result = self.begin_result(0, 0, xmax - xmin, ymax - ymin) lay = result.layers[0] # This assumes the file has been fully written We wait a bit, just in case! time.sleep(self.DELAY) try: lay.load_from_file(self._temp_file_out) # XXX, tests for border render. # lay.load_from_file(self._temp_file_out, xmin, ymin) except RuntimeError: print("***POV ERROR WHILE READING OUTPUT FILE***") # Not needed right now, might only be useful if we find a way to use temp raw output of # pov 3.7 (in which case it might go under _test_wait()). ''' def update_image(): # possible the image wont load early on. try: lay.load_from_file(self._temp_file_out) # XXX, tests for border render. #lay.load_from_file(self._temp_file_out, xmin, ymin) #lay.load_from_file(self._temp_file_out, xmin, ymin) except RuntimeError: pass # Update while POV-Ray renders while True: # print("***POV RENDER LOOP***") # test if POV-Ray exists if self._process.poll() is not None: print("***POV PROCESS FINISHED***") update_image() break # user exit if self.test_break(): try: self._process.terminate() print("***POV PROCESS INTERRUPTED***") except OSError: pass break # Would be nice to redirect the output # stdout_value, stderr_value = self._process.communicate() # locks # check if the file updated new_size = os.path.getsize(self._temp_file_out) if new_size != prev_size: update_image() prev_size = new_size time.sleep(self.DELAY) ''' self.end_result(result) else: print("***NO POV OUTPUT IMAGE***") print("***POV INPUT FILE WRITTEN***") # print(filename_log) #bring the pov log to blender console with proper path? try: with open( self._temp_file_log, encoding='utf-8' ) as f: # The with keyword automatically closes the file when you are done msg = f.read() if isinstance(msg, str): stdmsg = msg #decoded = False elif type(msg) == bytes: #stdmsg = msg.split('\n') stdmsg = msg.encode('utf-8', "replace") # stdmsg = msg.encode("utf-8", "replace") # stdmsg = msg.decode(encoding) # decoded = True # msg.encode('utf-8').decode('utf-8') stdmsg.replace("\t", " ") print(stdmsg) # console_write(stdmsg) # todo fix segfault and use except FileNotFoundError: print("No render log to read") self.update_stats("", "") if scene.pov.tempfiles_enable or scene.pov.deletefiles_enable: self._cleanup() sound_on = bpy.context.preferences.addons[__package__].preferences.use_sounds finished_render_message = "\'Et Voilà!\'" if platform.startswith('win') and sound_on: # Could not find tts Windows command so playing beeps instead :-) # "Korobeiniki"(Коробе́йники) # aka "A-Type" Tetris theme import winsound winsound.Beep(494, 250) # B winsound.Beep(370, 125) # F winsound.Beep(392, 125) # G winsound.Beep(440, 250) # A winsound.Beep(392, 125) # G winsound.Beep(370, 125) # F# winsound.Beep(330, 275) # E winsound.Beep(330, 125) # E winsound.Beep(392, 125) # G winsound.Beep(494, 275) # B winsound.Beep(440, 125) # A winsound.Beep(392, 125) # G winsound.Beep(370, 275) # F winsound.Beep(370, 125) # F winsound.Beep(392, 125) # G winsound.Beep(440, 250) # A winsound.Beep(494, 250) # B winsound.Beep(392, 250) # G winsound.Beep(330, 350) # E time.sleep(0.5) winsound.Beep(440, 250) # A winsound.Beep(440, 150) # A winsound.Beep(523, 125) # D8 winsound.Beep(659, 250) # E8 winsound.Beep(587, 125) # D8 winsound.Beep(523, 125) # C8 winsound.Beep(494, 250) # B winsound.Beep(494, 125) # B winsound.Beep(392, 125) # G winsound.Beep(494, 250) # B winsound.Beep(440, 150) # A winsound.Beep(392, 125) # G winsound.Beep(370, 250) # F# winsound.Beep(370, 125) # F# winsound.Beep(392, 125) # G winsound.Beep(440, 250) # A winsound.Beep(494, 250) # B winsound.Beep(392, 250) # G winsound.Beep(330, 300) # E # Mac supports natively say command elif platform == "darwin": # We don't want the say command to block Python, # so we add an ampersand after the message # but if the os TTS package isn't up to date it # still does thus, the try except clause try: os.system("say -v Amelie %s &" % finished_render_message) except BaseException as e: print(e.__doc__) print("your Mac may need an update, try to restart computer") pass # While Linux frequently has espeak installed or at least can suggest # Maybe windows could as well ? elif platform == "linux": # We don't want the espeak command to block Python, # so we add an ampersand after the message # but if the espeak TTS package isn't installed it # still does thus, the try except clause try: os.system("echo %s | espeak &" % finished_render_message) except BaseException as e: print(e.__doc__) pass classes = ( PovRender, ) def register(): for cls in classes: register_class(cls) def unregister(): for cls in reversed(classes): unregister_class(cls)