# ====================== BEGIN GPL LICENSE BLOCK ====================== # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ======================= END GPL LICENSE BLOCK ======================== # ######################################## # Render Cube Map # # Dalai Felinto # -- # blendernetwork.org/dalai-felinto # www.dalaifelinto.com # # Original code: # Rio de Janeiro, September 2015 # # Latest update: # Rio de Janeiro, July 2016 # ######################################## import bpy from bpy.app.handlers import persistent from bpy.types import ( Operator, Panel, ) from bpy.props import ( BoolProperty, ) bl_info = { "name": "Cube Map", "author": "Dalai Felinto", "version": (1, 0), "blender": (2, 80, 0), "location": "Render Panel", "description": "", "warning": "Needs Updating", "doc_url": "https://github.com/dfelinto/render_cube_map", "tracker_url": "", "category": "Render"} # ############################################################ # Global Check # ############################################################ def do_run(cube_map, use_force): if not (cube_map.use_cube_map or use_force): return False if cube_map.is_enabled and not use_force: return False return True # ############################################################ # Callbacks # ############################################################ class NodeTree: def __init__(self, scene): self._use_nodes = scene.use_nodes self._use_compositing = scene.render.use_compositing self._nodes_mute = {} self._scene = scene self._scene.render.use_compositing = True if not self._use_nodes: scene.use_nodes = True self._muteNodes() else: self._storeNodes() self._muteNodes() def _storeNodes(self): """ store the existent nodes and if they are muted """ nodes = self._scene.node_tree.nodes for node in nodes: self._nodes_mute[hash(node)] = node.mute def _muteNodes(self): """ mute all the existent nodes """ nodes = self._scene.node_tree.nodes for node in nodes: node.mute = True def cleanupScene(self): """ remove all the new nodes, and unmute original ones """ scene = self._scene scene.use_nodes = self._use_nodes scene.render.use_compositing = self._use_compositing self._cleanNodes() self._unMuteNodes() def _cleanNodes(self): """ remove all the nodes created temporarily """ nodes = self._scene.node_tree.nodes to_del = [] keys = self._nodes_mute.keys() for node in nodes: if hash(node) not in keys: to_del.append(node) for node in to_del: nodes.remove(node) def _unMuteNodes(self): """ unmute all the existent nodes """ nodes = self._scene.node_tree.nodes for node in nodes: node.mute = self._nodes_mute[hash(node)] class View: def __init__(self, name, euler_rotation): self._name = name self._collection = None self._scene = None self._scene_camera = None self._node = None self._camera = None self._euler_rotation = euler_rotation def setScene(self, scene): scene.name = self._name self._scene = scene scene.cube_map.use_cube_map = False scene.render.use_compositing = False self._setFilepath() def _setFilepath(self): import os filepath = self._scene.render.filepath dirname = os.path.dirname(filepath) basename = os.path.basename(filepath) path = os.path.join(dirname, "{0}{1}".format(self._name, basename)) self._scene.render.filepath = path def setNode(self, node, links, node_output): node.name = self._name node.label = self._name node.scene = self._scene self._node = node # TODO if there were nodetrees, duplicate them here # connect to output _input = node_output.layer_slots.new(self._name) links.new(node.outputs[0], _input) def setCamera(self, data, loc, zed): self._scene_camera = self._scene.camera self._camera = bpy.data.objects.new(self._name, data) self._collection.objects.link(self._camera) rotation = self._euler_rotation.copy() rotation.z += zed self._camera.rotation_euler = rotation self._camera.location = loc # change scene camera self._scene.camera = self._camera def resetCamera(self): self._collection.objects.unlink(self._camera) bpy.data.objects.remove(self._camera) self._camera = None @property def scene(self): return self._scene @property def name(self): return self._name @persistent def cube_map_render_init(scene, use_force=False): """ setup the cube map settings for all the render frames """ from mathutils import Euler from math import pi half_pi = pi * 0.5 cube_map = scene.cube_map if not do_run(cube_map, use_force): return main_scene = scene hashes = [hash(scene) for scene in bpy.data.scenes] views_raw = ( ( 'NORTH_', Euler((half_pi, 0.0, 0.0)), cube_map.use_view_north, ), ( 'SOUTH_', Euler((half_pi, 0.0, pi)), cube_map.use_view_south, ), ( 'WEST_', Euler((half_pi, 0.0, half_pi)), cube_map.use_view_west, ), ( 'EAST_', Euler((half_pi, 0.0, -half_pi)), cube_map.use_view_east, ), ( 'ZENITH_', Euler((pi, 0.0, 0.0)), cube_map.use_view_zenith, ), ( 'NADIR_', Euler((0.0, 0.0, 0.0)), cube_map.use_view_nadir, ), ) views = [ View(name, euler) for (name, euler, use) in views_raw if use or not cube_map.is_advanced] for view in views: # create a scene per view # XXX : line below crashes Blender 2.80 bpy.ops.scene.new(type='LINK_COPY') scene = [ scene for scene in bpy.data.scenes if hash(scene) not in hashes][0] # mark the scene to remove it afterwards scene.cube_map.is_temporary = True hashes.append(hash(scene)) view.setScene(scene) # have Dalai to look at this? view._collection = bpy.context.collection # XXX TODO better fix # create a scene from scratch node_tree_data = NodeTree(main_scene) # created the necessary nodetrees there node_tree = main_scene.node_tree # output node node_output = node_tree.nodes.new('CompositorNodeOutputFile') node_output.inputs.clear() for view in views: node = node_tree.nodes.new('CompositorNodeRLayers') view.setNode(node, node_tree.links, node_output) # globals bpy.cube_map_node_tree_data = node_tree_data bpy.cube_map_views = views # ############################################################ # Cameras Setup # ############################################################ @persistent def cube_map_render_pre(scene, use_force=False): if not do_run(scene.cube_map, use_force): return from math import radians camera = scene.camera data = camera.data.copy() data.lens_unit = 'FOV' data.angle = radians(90) data.type = 'PERSP' mat = camera.matrix_world loc = mat.to_translation() rot = mat.to_euler() zed = rot.z views = bpy.cube_map_views for view in views: view.setCamera(data, loc, zed) @persistent def cube_map_render_post(scene, use_force=False): if not do_run(scene.cube_map, use_force): return views = bpy.cube_map_views for view in views: view.resetCamera() # ############################################################ # Clean-Up # ############################################################ @persistent def cube_map_render_cancel(scene): cube_map_cleanup(scene) @persistent def cube_map_render_complete(scene): cube_map_cleanup(scene) def cube_map_cleanup(scene, use_force=False): """ remove all the temporary data created for the cube map """ if not do_run(scene.cube_map, use_force): return bpy.cube_map_node_tree_data.cleanupScene() del bpy.cube_map_node_tree_data del bpy.cube_map_views bpy.app.handlers.scene_update_post.append(cube_map_post_update_cleanup) def cube_map_post_update_cleanup(scene): """ delay removal of scenes (otherwise we get a crash) """ scenes_temp = [ scene for scene in bpy.data.scenes if scene.cube_map.is_temporary] if not scenes_temp: bpy.app.handlers.scene_update_post.remove(cube_map_post_update_cleanup) else: scenes_temp[0].user_clear() try: bpy.data.scenes.remove(scenes_temp[0], do_unlink=False) except TypeError: bpy.data.scenes.remove(scenes_temp[0]) # ############################################################ # Setup Operator # ############################################################ class CubeMapSetup(Operator): """""" bl_idname = "render.cube_map_setup" bl_label = "Cube Map Render Setup" bl_description = "" action: bpy.props.EnumProperty( description="", items=(("SETUP", "Setup", "Created linked scenes and setup cube map"), ("RESET", "Reset", "Delete added scenes"), ), default="SETUP", options={'SKIP_SAVE'}, ) @classmethod def poll(cls, context): return True def setup(self, window, scene): cube_map = scene.cube_map cube_map.is_enabled = True cube_map_render_init(scene, use_force=True) cube_map_render_pre(scene, use_force=True) # set initial scene back as the main scene window.screen.scene = scene def reset(self, scene): cube_map = scene.cube_map cube_map.is_enabled = False cube_map_render_post(scene, use_force=True) cube_map_cleanup(scene, use_force=True) def invoke(self, context, event): scene = context.scene cube_map = scene.cube_map is_enabled = cube_map.is_enabled if self.action == 'RESET': if is_enabled: if cube_map.is_temporary: self.report( {'ERROR'}, "Cannot reset cube map from one of " "the created scenes") return {'CANCELLED'} else: self.reset(scene) return {'FINISHED'} else: self.report({'ERROR'}, "Cube Map render is not setup") return {'CANCELLED'} else: # SETUP if is_enabled: self.report({'ERROR'}, "Cube Map render is already setup") return {'CANCELLED'} else: self.setup(context.window, scene) return {'FINISHED'} # ############################################################ # User Interface # ############################################################ class RENDER_PT_cube_map(Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "render" bl_label = "Cube Map" @classmethod def poll(cls, context): scene = context.scene return scene and (scene.render.engine != 'BLENDER_GAME') def draw_header(self, context): self.layout.prop(context.scene.cube_map, "use_cube_map", text="") def draw(self, context): layout = self.layout col = layout.column() scene = context.scene cube_map = scene.cube_map if not cube_map.is_enabled: col.operator( "render.cube_map_setup", text="Scene Setup").action = 'SETUP' else: col.operator( "render.cube_map_setup", text="Scene Reset", icon="X").action = 'RESET' col = layout.column() col.active = cube_map.use_cube_map col.prop(cube_map, "is_advanced") if cube_map.is_advanced: box = col.box() box.active = cube_map.use_cube_map and cube_map.is_advanced row = box.row() row.prop(cube_map, "use_view_north") row.prop(cube_map, "use_view_west") row.prop(cube_map, "use_view_zenith") row = box.row() row.prop(cube_map, "use_view_south") row.prop(cube_map, "use_view_east") row.prop(cube_map, "use_view_nadir") # ############################################################ # Scene Properties # ############################################################ class CubeMapInfo(bpy.types.PropertyGroup): use_cube_map: BoolProperty( name="Cube Map", default=False, ) is_temporary: BoolProperty( name="Temporary", default=False, ) is_enabled: BoolProperty( name="Enabled", default=False, ) # per view settings is_advanced: BoolProperty( name="Advanced", default=False, description="Decide which views to render", ) use_view_north: BoolProperty( name="North", default=True, ) use_view_south: BoolProperty( name="South", default=True, ) use_view_west: BoolProperty( name="West", default=True, ) use_view_east: BoolProperty( name="East", default=True, ) use_view_zenith: BoolProperty( name="Zenith", default=True, ) use_view_nadir: BoolProperty( name="Nadir", default=True, ) # ############################################################ # Un/Registration # ############################################################ def register(): bpy.utils.register_class(CubeMapInfo) bpy.utils.register_class(CubeMapSetup) bpy.types.Scene.cube_map = bpy.props.PointerProperty( name="cube_map", type=CubeMapInfo, options={'HIDDEN'}, ) bpy.utils.register_class(RENDER_PT_cube_map) bpy.app.handlers.render_init.append(cube_map_render_init) bpy.app.handlers.render_pre.append(cube_map_render_pre) bpy.app.handlers.render_post.append(cube_map_render_post) bpy.app.handlers.render_cancel.append(cube_map_render_cancel) bpy.app.handlers.render_complete.append(cube_map_render_complete) def unregister(): bpy.utils.unregister_class(CubeMapInfo) bpy.utils.unregister_class(CubeMapSetup) bpy.utils.unregister_class(RENDER_PT_cube_map) bpy.app.handlers.render_init.remove(cube_map_render_init) bpy.app.handlers.render_pre.remove(cube_map_render_pre) bpy.app.handlers.render_post.remove(cube_map_render_post) bpy.app.handlers.render_cancel.remove(cube_map_render_cancel) bpy.app.handlers.render_complete.remove(cube_map_render_complete) del bpy.types.Scene.cube_map if __name__ == '__main__': register()