618 lines
16 KiB
Python
618 lines
16 KiB
Python
# ====================== 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 ========================
|
|
|
|
# <pep8 compliant>
|
|
|
|
# ########################################
|
|
# 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()
|