Skip to content

Instantly share code, notes, and snippets.

@strike-digital
Created October 18, 2021 14:53
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save strike-digital/0b5a2c0111f1aec3923b5fd17d1881e2 to your computer and use it in GitHub Desktop.
Save strike-digital/0b5a2c0111f1aec3923b5fd17d1881e2 to your computer and use it in GitHub Desktop.
File for adding a camera info node to geometry nodes fields.
# Copyright (C) 2021 'BD3D DIGITAL DESIGN, SLU'
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
import bpy
bl_info = {
"name": "'Camera info' for Geometry-Node",
"author": "BD3D, Andrew Stevenson",
"description": "This plugin adds an extra node for Geometry-Node that will Get info about a camera",
"blender": (3, 0, 0),
"version": (1, 0, 0),
"location": "Node Editor > Geometry Node > Add Menu > Extra",
"warning": "",
"category": "Node"
}
# """
# About creating GeometryNodeCustomGroup:
# >Here are the possibilities:
# - you can either create a custom interface that interact with a nodegroup
# - or create simple input node, this plugin is only creating input values. all boiler plate below is dedicated to output sockets.
# > if you want to process data, forget about it:
# - currently there's no way to get the value out of a socket, not sure how they could translate field to python.
# - writing simple output value is possible, forget about fields tho.
# > update management is not ideal
# - socket_value_update() function should send us signal when a socket value is being updated, the api is dead for now
# - to update, rely on handlers or msgbus ( https://docs.blender.org/api/blender2.8/bpy.app.handlers.html?highlight=handler# module-bpy.app.handlers )
# > socket.type is read only, everything is hardcoded in operators
# - to change socket type, we forced to use operator `bpy.ops.node.tree_socket_change_type(in_out='IN', socket_type='')` + context 'override'. this is far from ideal.
# this means that changing socket type outside the node editor context is not possible.
# > in order to change the default value of an output, nodegroup.outputs[n].default value won't work use, api is confusing, it is done via the nodegroup.nodes instead:
# - nodegroup.nodes["Group Output"].inputs[n].default_value ->see boiler plate functions i wrote below
# > Warning `node_groups[x].outputs.new("NodeSocketBool","test")` is tricky, type need to be exact, no warning upon error, will just return none
# About this script:
# >You will note that there is an extra attention to detail in order to not register handlers twice
# >You will note that there is an extra attention in the extension of the Add menu with this new 'Extra' category.
# In, my opinion all plugin nodes should be in this "Extra" menu.
# Feel free to reuse the menu registration snippets so all custom node makers can share the 'Extra' menu without confilcts.
# """
#######################################################
# BOILER PLATE
#######################################################
def get_socket_value(ng, idx):
return ng.nodes["Group Output"].inputs[idx].default_value
def set_socket_value(ng, idx, value=None):
ng.nodes["Group Output"].inputs[idx].default_value = value
return ng.nodes["Group Output"].inputs[idx].default_value
def set_socket_label(ng, idx, label=None):
ng.outputs[idx].name = str(label)
return None
def get_socket_type(ng, idx):
return ng.outputs[idx].type
def set_socket_type(ng, idx, socket_type="NodeSocketFloat"):
"""set socket type via bpy.ops.node.tree_socket_change_type() with manual override, context MUST be the geometry node editor"""
snode = bpy.context.space_data
if snode is None:
return None
# forced to do a ugly override like this... eww
restore_override = {"node_tree": snode.node_tree, "pin": snode.pin}
snode.pin = True
snode.node_tree = ng
ng.active_output = idx
bpy.ops.node.tree_socket_change_type(
in_out='OUT', socket_type=socket_type
) # operator override is best, but which element do we need to override, not sure what the cpp operator need..
# then restore... all this will may some signal to depsgraph
for api, obj in restore_override.items():
setattr(snode, api, obj)
return None
def create_socket(ng, socket_type="NodeSocketFloat", socket_name="Value"):
socket = ng.outputs.new(socket_type, socket_name)
return socket
def remove_socket(ng, idx):
todel = ng.outputs[idx]
ng.outputs.remove(todel)
return None
def create_new_nodegroup(name, sockets={}):
"""create new nodegroup with outputs from given dict {"type":"name",}, make sure given type are correct"""
ng = bpy.data.node_groups.new(name=name, type="GeometryNodeTree")
in_node = ng.nodes.new("NodeGroupInput")
in_node.location.x -= 200
out_node = ng.nodes.new("NodeGroupOutput")
out_node.location.x += 200
for socket_type, socket_name in sockets.items():
create_socket(ng, socket_type=socket_type, socket_name=socket_name)
return ng
# def import_nodegroup(groupname, source_blend="extra_node_camera_info.blend",):
# """import an existing nodegroup"""
# import os
# python_path = os.path.dirname(os.path.realpath(__file__))
# lib_file = os.path.join(python_path,source_blend)
# with bpy.data.libraries.load(lib_file,link=False) as (data_from,data_to):
# data_to.node_groups.append(groupname)
# group = bpy.data.node_groups[groupname]
# group.use_fake_user = True
# return group
#######################################################
# CUSTOM NODE
#######################################################
class EXTRANODECAMERAINFO_NG_camera_info(bpy.types.GeometryNodeCustomGroup):
bl_idname = "GeometryNodeCameraInfo"
bl_label = "Camera info"
error: bpy.props.BoolProperty(default=False,)
socket_type: bpy.props.StringProperty(default="NodeSocketBool")
debug_update_counter: bpy.props.IntProperty() # visual aid debug
use_scene_cam: bpy.props.BoolProperty()
def camera_obj_poll(self, obj):
return True if obj.type == "CAMERA" else False
camera_obj: bpy.props.PointerProperty(type=bpy.types.Object, poll=camera_obj_poll)
@classmethod
def poll(cls, context): # mandatory with geonode
return True
def init(self, context):
"""this fct run when appending the node for the first time"""
name = f".{self.bl_idname}"
if name not in bpy.data.node_groups.keys():
ng = create_new_nodegroup(name, sockets={
"NodeSocketObject": "Camera object",
})
for name in ["Field of view", "Shift x", "Shift y", "Clip start", "Clip end"]:
create_socket(ng, socket_name=name)
else:
ng = bpy.data.node_groups[name].copy()
self.node_tree = ng
self.label = self.bl_label
# self.width = 250
# mark an update signal so handler fct do not need to loop every single nodegroups
bpy.context.space_data.node_tree["extra_node_camera_info_update_needed"] = True
return None
def copy(self, node):
"""fct run when dupplicating the node"""
self.node_tree = node.node_tree.copy()
return None
def update(self):
"""generic update function"""
cam_obj = bpy.context.scene.camera if self.use_scene_cam else self.camera_obj
set_socket_value(self.node_tree, 0, cam_obj)
if cam_obj and cam_obj.data:
cam = cam_obj.data
set_socket_value(self.node_tree, 1, cam.angle)
set_socket_value(self.node_tree, 2, cam.shift_x)
set_socket_value(self.node_tree, 3, cam.shift_y)
set_socket_value(self.node_tree, 4, cam.clip_start)
set_socket_value(self.node_tree, 5, cam.clip_end)
self.debug_update_counter += 1
return None
# def socket_value_update(self,context):
# """dead api, revive me please?"""
# return None
def draw_label(self,):
"""node label"""
return "Camera info"
def draw_buttons(self, context, layout):
"""node interface drawing"""
row = layout.row(align=True)
row.alert = self.error
sub = row.row(align=True)
sub.active = not self.use_scene_cam
if self.use_scene_cam:
sub.prop(bpy.context.scene, "camera", text="")
else:
sub.prop(self, "camera_obj", text="")
row.prop(self, "use_scene_cam", text="", icon="SCENE_DATA")
if bpy.context.preferences.addons["extra_node_camera_info"].preferences.debug:
box = layout.column()
box.active = False
box.prop(self, "node_tree", text="")
box.prop(self, "debug_update_counter", text="update count")
return None
#######################################################
# HANDLER UPDATE
#######################################################
@bpy.app.handlers.persistent
def extra_node_camera_info_depsgraph(scene, desp): # used for Api node, if allowed!
"""update on depsgraph change"""
if bpy.context.preferences.addons["extra_node_camera_info"].preferences.debug:
print("extra_node_camera_info: depsgraph signal")
# automatic update for Camera info only if allowed
if not bpy.context.preferences.addons["extra_node_camera_info"].preferences.auto_evaluate_py:
return None
# search for nodes all over data and update
for n in [
n for ng in bpy.data.node_groups if ("extra_node_camera_info_update_needed" in ng) for n in ng.nodes
if (n.bl_idname == "GeometryNodeCameraInfo")
]:
n.update()
return None
@bpy.app.handlers.persistent
def extra_node_camera_info_frame_pre(scene, desp): # used for Volume and Api Node!
"""update on frame change"""
if bpy.context.preferences.addons["extra_node_camera_info"].preferences.debug:
print("extra_node_camera_info: frame_pre signal")
# automatic update for Camera info only if allowed
if not bpy.context.preferences.addons["extra_node_camera_info"].preferences.auto_evaluate_py:
return None
# search for nodes all over data and update
for n in [
n for ng in bpy.data.node_groups if ("extra_node_camera_info_update_needed" in ng) for n in ng.nodes
if (n.bl_idname == "GeometryNodeCameraInfo")
]:
n.update()
return None
def all_handlers(name=False):
"""return a list of handler stored in .blend"""
return_list = []
for oh in bpy.app.handlers:
try:
for h in oh:
return_list.append(h)
except:
pass
return return_list
def register_handlers(status):
"""register dispatch for handlers"""
if (status == "register"):
all_handler_names = [h.__name__ for h in all_handlers()]
# depsgraph
if "extra_node_camera_info_depsgraph" not in all_handler_names:
bpy.app.handlers.depsgraph_update_post.append(extra_node_camera_info_depsgraph)
# frame_change
if "extra_node_camera_info_frame_pre" not in all_handler_names:
bpy.app.handlers.frame_change_pre.append(extra_node_camera_info_frame_pre)
# render
# if extra_node_camera_info_render_pre not in all_handlers():
# bpy.app.handlers.render_pre.append(extra_node_camera_info_render_pre)
# if extra_node_camera_info_render_post not in all_handlers():
# bpy.app.handlers.render_post.append(extra_node_camera_info_render_post)
# blend open
# if extra_node_camera_info_load_post not in all_handlers():
# bpy.app.handlers.load_post.append(extra_node_camera_info_load_post)
return None
elif (status == "unregister"):
for h in all_handlers():
# depsgraph
if (h.__name__ == "extra_node_camera_info_depsgraph"):
bpy.app.handlers.depsgraph_update_post.remove(h)
# frame_change
if (h.__name__ == "extra_node_camera_info_frame_pre"):
bpy.app.handlers.frame_change_pre.remove(h)
# # render
# if(h==extra_node_camera_info_render_pre):
# bpy.app.handlers.render_pre.remove(h)
# if(h==extra_node_camera_info_render_post):
# bpy.app.handlers.render_post.remove(h)
# # blend open
# if(h==extra_node_camera_info_load_post):
# bpy.app.handlers.load_post.remove(h)
return None
#######################################################
# EXTEND MENU
#######################################################
# extra menu
def extra_geonode_menu(self, context):
"""extend NODE_MT_add with new extra menu"""
self.layout.menu("NODE_MT_category_GEO_EXTRA", text="Extra")
return None
class NODE_MT_category_GEO_EXTRA(bpy.types.Menu):
bl_idname = "NODE_MT_category_GEO_EXTRA"
bl_label = ""
@classmethod
def poll(cls, context):
return (bpy.context.space_data.tree_type == "GeometryNodeTree")
def draw(self, context):
return None
# extra menu extension
def extra_node_camera_info(self, context):
"""extend extra menu with new node"""
op = self.layout.operator("node.add_node", text="Camera info")
op.type = "GeometryNodeCameraInfo"
op.use_transform = True
# register
def register_menus(status):
"""register extra menu, if not already, append item, if not already"""
if (status == "register"):
# register new extra menu class if not exists already, perhaps another plugin already implemented it
if "NODE_MT_category_GEO_EXTRA" not in bpy.types.__dir__():
bpy.utils.register_class(NODE_MT_category_GEO_EXTRA)
# extend add menu with extra menu if not already, _dyn_ui_initialize() will get appended drawing functions of a menu
add_menu = bpy.types.NODE_MT_add
if "extra_geonode_menu" not in [f.__name__ for f in add_menu._dyn_ui_initialize()]:
add_menu.append(extra_geonode_menu)
# extend extra menu with our custom nodes if not already
extra_menu = bpy.types.NODE_MT_category_GEO_EXTRA
if "extra_node_camera_info" not in [f.__name__ for f in extra_menu._dyn_ui_initialize()]:
extra_menu.append(extra_node_camera_info)
return None
elif (status == "unregister"):
add_menu = bpy.types.NODE_MT_add
extra_menu = bpy.types.NODE_MT_category_GEO_EXTRA
# remove our custom function to extra menu
for f in extra_menu._dyn_ui_initialize().copy():
if (f.__name__ == "extra_node_camera_info"):
extra_menu.remove(f)
# if extra menu is empty
if len(extra_menu._dyn_ui_initialize()) == 1:
# remove our extra menu item draw fct add menu
for f in add_menu._dyn_ui_initialize().copy():
if (f.__name__ == "extra_geonode_menu"):
add_menu.remove(f)
# unregister extra menu
bpy.utils.unregister_class(extra_menu)
return None
#######################################################
# PROPERTIES & PREFS
#######################################################
class EXTRANODECAMERAINFO_AddonPref(bpy.types.AddonPreferences):
"""addon_prefs = bpy.context.preferences.addons["extra_node_camera_info"].preferences"""
bl_idname = "extra_node_camera_info"
debug: bpy.props.BoolProperty(default=False)
auto_evaluate_py: bpy.props.BoolProperty(default=True)
convenience_exec1: bpy.props.StringProperty(default="rom mathutils import * ; from math import *") # fake
convenience_exec2: bpy.props.StringProperty(
default="D = bpy.data ; C = context = bpy.context ; scene = context.scene") # fake
convenience_exec3: bpy.props.StringProperty(default="")
# drawing part in ui module
def draw(self, context):
layout = self.layout
box = layout.box()
box.prop(self, "auto_evaluate_py", text="Auto Evaluation")
box.prop(self, "debug", text="Debug Mode")
convenience = box.column(align=True)
convenience.label(text="Convenience Execution:")
cexec = convenience.row()
cexec.enabled = False
cexec.prop(self, "convenience_exec1", text="")
cexec = convenience.row()
cexec.enabled = False
cexec.prop(self, "convenience_exec2", text="")
cexec = convenience.row()
cexec.active = False
cexec.prop(self, "convenience_exec3", text="")
return None
#######################################################
# INIT REGISTRATION
#######################################################
classes = [
EXTRANODECAMERAINFO_AddonPref,
EXTRANODECAMERAINFO_NG_camera_info,
]
def register():
# classes
for cls in classes:
bpy.utils.register_class(cls)
# extend add menu
register_menus("register")
# handlers
register_handlers("register")
return None
def unregister():
# handlers
register_handlers("unregister")
# extend add menu
register_menus("unregister")
# classes
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
return None
if __name__ == "__main__":
try:
unregister()
except:
pass
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment