├── .gitignore ├── LICENSE ├── README.md ├── add-ons ├── pie_menu_with_custom_op │ ├── pie_menu_with_custom_op_final.py │ └── pie_menu_with_custom_op_start.py ├── simple_custom_operator_with_props │ └── simple_custom_operator_with_props.py └── simple_custom_panel │ └── simple_custom_panel.py ├── art_projects └── truchet_tiles │ ├── truchet_tiles_done.py │ ├── truchet_tiles_done_bpybb.py │ └── truchet_tiles_start.py ├── assets └── images │ ├── color_slices.png │ ├── cube_loop.png │ ├── floret.png │ ├── flow_out.png │ ├── import_into_sequence_editor.png │ ├── in_or_out.png │ ├── loop_of_rings.png │ ├── shapeshifting.png │ ├── video_grid.png │ └── weave.png ├── automation ├── apply_color_palettes │ ├── apply_color_palettes_done.py │ └── apply_color_palettes_start.py └── basic_360_turntable_animation │ ├── basic_360_turntable_animation_done.py │ ├── basic_360_turntable_animation_done_bpybb.py │ └── basic_360_turntable_animation_start.py ├── bpybb └── set_up_world_sun_light │ ├── set_up_world_sun_light_end.py │ └── set_up_world_sun_light_start.py ├── brainstorming_reverse_key_lighting ├── script_done.py └── script_start.py ├── color_slices ├── color_slices_part1_done.py ├── color_slices_part2_done.py ├── color_slices_part2_start.py ├── color_slices_part3_done.py ├── color_slices_part3_start.py ├── color_slices_part4_done.py ├── color_slices_part4_start.py ├── color_slices_part5_done.py ├── color_slices_part5_start.py ├── color_slices_part6_done.py ├── color_slices_part6_start.py ├── color_slices_part7_done.py ├── color_slices_part7_start.py ├── color_slices_part8_done.py └── color_slices_part8_start.py ├── compositor_import_image_sequence ├── compositor_import_image_sequence_done.py └── compositor_import_image_sequence_start.py ├── cube_loop ├── cube_loop_done.py └── cube_loop_start.py ├── floret ├── script_done.py ├── script_done_bpybb.py └── script_start.py ├── flow_out ├── flow_out_done.py └── flow_out_start.py ├── geo_nodes └── subdivided_triangulated_cube │ ├── subdivided_triangulated_cube_part_1_done.py │ ├── subdivided_triangulated_cube_part_1_start.py │ └── subdivided_triangulated_cube_part_2_done.py ├── hex_delay ├── hex_delay_done.py └── hex_delay_start.py ├── holder ├── script_done.py ├── script_done_bpybb.py └── script_start.py ├── in_or_out ├── in_or_out_done.py ├── in_or_out_done_bpybb.py └── in_or_out_start.py ├── loop_of_rings ├── loop_of_rings_done.py └── loop_of_rings_start.py ├── outline ├── outline_done.py ├── outline_done_bpybb.py └── outline_start.py ├── sequence_editor_frame_import ├── sequence_editor_frame_import_done.py └── sequence_editor_frame_import_start.py ├── sequence_editor_stitched_together_clips ├── sequence_editor_stitched_together_clips_done.py └── sequence_editor_stitched_together_clips_start.py ├── shapeshifting_loop ├── shapeshifting_done.py ├── shapeshifting_refactor.py ├── shapeshifting_refactor_bpybb.py └── shapeshifting_start.py ├── stack_overflow ├── stack_overflow_done.py ├── stack_overflow_done_bpybb.py └── stack_overflow_start.py ├── stack_spin ├── stack_spin_done.py └── stack_spin_start.py ├── video_grid ├── video_grid_done.py └── video_grid_start.py ├── weave ├── weave_done.py ├── weave_hdri_done.py └── weave_start.py └── working_with_color_palettes ├── color_palettes_done.py └── color_palettes_start.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CGArtPython 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender + Python 2 | A tutorial series about creating art with Blender Python scripts 3 | 4 | --- 5 | 6 | # Blender+Python: Color Slices Tutorial Series 7 | 8 | [Watch tutorial series](https://www.youtube.com/watch?v=ys9vqbatyj4&list=PLB8-FQgROBmlzQ7Xkq4YU7u08Zh3iuyPD) 9 | 10 | ![Blender+Python: Color Slices Tutorial Series image](/assets/images/color_slices.png) 11 | 12 | --- 13 | 14 | # Abstract Animation Loop with Python in Blender 15 | 16 | [Watch tutorial](https://youtu.be/zRZn0VjsdH4) 17 | 18 | ![Blender Python Scripting: Outgoing Circle Loop image](/assets/images/loop_of_rings.png) 19 | 20 | --- 21 | 22 | # Python + Blender: Metaball animation created with a script 23 | 24 | [Watch tutorial](https://youtu.be/rIhXHSdMWmc) 25 | 26 | ![Python + Blender: Metaball animation created with a script image](/assets/images/in_or_out.png) 27 | 28 | --- 29 | 30 | # Python + Blender: How to animate a phyllotaxis pattern 31 | 32 | [Watch tutorial](https://youtu.be/ml1hwMYskhc) 33 | 34 | ![Python + Blender: How to animate a phyllotaxis pattern](/assets/images/floret.png) 35 | 36 | --- 37 | 38 | # Casting to a sphere animation loop created with a script 39 | 40 | [Watch tutorial](https://youtu.be/J3yerCN1-I4) 41 | 42 | ![Casting to a sphere animation loop created with a script image](/assets/images/shapeshifting.png) 43 | 44 | --- 45 | 46 | # Tilt animation loop created with Python in Blender 47 | 48 | [Watch tutorial](https://www.youtube.com/watch?v=bWmnbEwEpqk) 49 | 50 | ![Tilt animation loop created with Python in Blender image](/assets/images/weave.png) 51 | 52 | --- 53 | 54 | # Blender Python Scripting: Wireframe Cube Loop 55 | 56 | [Watch tutorial](https://www.youtube.com/watch?v=GgX7rGcrHVI) 57 | 58 | ![Blender Python Scripting: Wireframe Cube Loop image](/assets/images/cube_loop.png) 59 | 60 | --- 61 | 62 | # Python for Blender: Scripting a Video Grid 63 | 64 | [Watch tutorial](https://www.youtube.com/watch?v=1toJeVNOdnk) 65 | 66 | ![Python for Blender: Scripting a Video Grid image](/assets/images/video_grid.png) 67 | 68 | --- 69 | 70 | # Python for Blender: How to import images into Blender’s video sequence editor 71 | 72 | [Watch tutorial](https://www.youtube.com/watch?v=DuiFrvs10TI) 73 | 74 | ![Python for Blender: How to import images into Blender’s video sequence editor image](/assets/images/import_into_sequence_editor.png) 75 | -------------------------------------------------------------------------------- /add-ons/pie_menu_with_custom_op/pie_menu_with_custom_op_final.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Pie Menu: Template", 3 | "description": "Pie menu example", 4 | "author": "Viktor Stepanov", 5 | "version": (0, 1, 1), 6 | "blender": (2, 80, 0), 7 | "location": "3D View", 8 | "warning": "", 9 | "doc_url": "", 10 | "category": "Development", 11 | } 12 | 13 | import bpy 14 | from bpy.types import Menu 15 | 16 | 17 | def add_subdiv_modifier(subdiv_viewport_levels, subdiv_render_levels): 18 | obj = bpy.context.active_object 19 | 20 | if obj is None: 21 | print("warning: no active object") 22 | return 23 | 24 | if obj.type != "MESH": 25 | print("warning: the active object need to be a mesh") 26 | return 27 | 28 | bpy.ops.object.modifier_add(type="SUBSURF") 29 | bpy.context.object.modifiers["Subdivision"].levels = subdiv_viewport_levels 30 | bpy.context.object.modifiers["Subdivision"].render_levels = subdiv_render_levels 31 | 32 | 33 | class MESH_OT_add_subdiv_mod(bpy.types.Operator): 34 | """Add a subdivision surf modifier to the active mesh""" 35 | 36 | bl_idname = "mesh.add_subdiv_mod" 37 | bl_label = "Add Subdivision Surf Modifier to the Active Mesh Object" 38 | bl_options = {"REGISTER", "UNDO"} 39 | 40 | subdiv_viewport_lvl: bpy.props.IntProperty( 41 | name="Subdiv Viewport", 42 | default=1, 43 | min=1, 44 | max=3, 45 | description="The Subdivision Levels applied in the Viewport", 46 | ) 47 | 48 | subdiv_render_lvl: bpy.props.IntProperty( 49 | name="Subdiv Render", 50 | default=3, 51 | min=3, 52 | max=7, 53 | description="The Subdivision Levels applied during the Viewport", 54 | ) 55 | 56 | def execute(self, context): 57 | add_subdiv_modifier(self.subdiv_viewport_lvl, self.subdiv_render_lvl) 58 | 59 | return {"FINISHED"} 60 | 61 | 62 | class VIEW3D_MT_PIE_template(Menu): 63 | # label is displayed at the center of the pie menu. 64 | bl_label = "Pie menu example" 65 | 66 | def draw(self, context): 67 | layout = self.layout 68 | 69 | pie = layout.menu_pie() 70 | 71 | column = pie.split().column() 72 | column.operator("mesh.primitive_torus_add", text="Add Torus", icon="MESH_TORUS") 73 | column.operator("mesh.primitive_plane_add", text="Add Plane", icon="MESH_PLANE") 74 | 75 | column = pie.split().column() 76 | column.operator("mesh.add_subdiv_mod", text="Add Subdiv Mod", icon="MOD_SUBSURF") 77 | column.operator("object.shade_smooth", text="Shade Smooth", icon="MOD_SMOOTH") 78 | 79 | 80 | global_addon_keymaps = [] 81 | 82 | 83 | def register(): 84 | bpy.utils.register_class(MESH_OT_add_subdiv_mod) 85 | bpy.utils.register_class(VIEW3D_MT_PIE_template) 86 | 87 | window_manager = bpy.context.window_manager 88 | if window_manager.keyconfigs.addon: 89 | keymap = window_manager.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") 90 | 91 | keymap_item = keymap.keymap_items.new("wm.call_menu_pie", "A", "PRESS", ctrl=True, alt=True) 92 | keymap_item.properties.name = "VIEW3D_MT_PIE_template" 93 | 94 | # save the key map to deregister later 95 | global_addon_keymaps.append((keymap, keymap_item)) 96 | 97 | 98 | def unregister(): 99 | bpy.utils.unregister_class(VIEW3D_MT_PIE_template) 100 | bpy.utils.unregister_class(MESH_OT_add_subdiv_mod) 101 | 102 | window_manager = bpy.context.window_manager 103 | if window_manager and window_manager.keyconfigs and window_manager.keyconfigs.addon: 104 | for keymap, keymap_item in global_addon_keymaps: 105 | keymap.keymap_items.remove(keymap_item) 106 | 107 | global_addon_keymaps.clear() 108 | 109 | 110 | if __name__ == "__main__": 111 | register() 112 | -------------------------------------------------------------------------------- /add-ons/pie_menu_with_custom_op/pie_menu_with_custom_op_start.py: -------------------------------------------------------------------------------- 1 | """ 2 | This code is based on a built-in Blender Python Tempalte - "UI Pie Menu" 3 | """ 4 | 5 | bl_info = { 6 | "name": "Pie Menu: Template", 7 | "description": "Pie menu example", 8 | "author": "Viktor Stepanov", 9 | "version": (0, 1, 1), 10 | "blender": (2, 80, 0), 11 | "location": "3D View", 12 | "warning": "", 13 | "doc_url": "", 14 | "category": "Development", 15 | } 16 | 17 | import bpy 18 | from bpy.types import Menu 19 | 20 | # spawn an edit mode selection pie (run while object is in edit mode to get a valid output) 21 | 22 | 23 | class VIEW3D_MT_PIE_template(Menu): 24 | # label is displayed at the center of the pie menu. 25 | bl_label = "Select Mode" 26 | 27 | def draw(self, context): 28 | layout = self.layout 29 | 30 | pie = layout.menu_pie() 31 | # operator_enum will just spread all available options 32 | # for the type enum of the operator on the pie 33 | pie.operator_enum("mesh.select_mode", "type") 34 | 35 | 36 | global_addon_keymaps = [] 37 | 38 | 39 | def register(): 40 | bpy.utils.register_class(VIEW3D_MT_PIE_template) 41 | 42 | window_manager = bpy.context.window_manager 43 | if window_manager.keyconfigs.addon: 44 | keymap = window_manager.keyconfigs.addon.keymaps.new(name="3D View", space_type="VIEW_3D") 45 | 46 | keymap_item = keymap.keymap_items.new("wm.call_menu_pie", "A", "PRESS", ctrl=True, alt=True) 47 | keymap_item.properties.name = "VIEW3D_MT_PIE_template" 48 | 49 | # save the key map to deregister later 50 | global_addon_keymaps.append((keymap, keymap_item)) 51 | 52 | 53 | def unregister(): 54 | bpy.utils.unregister_class(VIEW3D_MT_PIE_template) 55 | 56 | window_manager = bpy.context.window_manager 57 | if window_manager and window_manager.keyconfigs and window_manager.keyconfigs.addon: 58 | for keymap, keymap_item in global_addon_keymaps: 59 | keymap.keymap_items.remove(keymap_item) 60 | 61 | global_addon_keymaps.clear() 62 | 63 | 64 | if __name__ == "__main__": 65 | register() 66 | 67 | # bpy.ops.wm.call_menu_pie(name="VIEW3D_MT_PIE_template") 68 | -------------------------------------------------------------------------------- /add-ons/simple_custom_operator_with_props/simple_custom_operator_with_props.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/0_QskeU8CPo 3 | """ 4 | 5 | bl_info = { 6 | "name": "My Custom Panel", 7 | "author": "Victor Stepanov", 8 | "version": (0, 0, 1), 9 | "blender": (2, 80, 0), 10 | "location": "3D Viewport > Sidebar > My Custom Panel category", 11 | "description": "My custom operator buttons", 12 | "category": "Development", 13 | } 14 | 15 | 16 | # give Python access to Blender's functionality 17 | import bpy 18 | 19 | 20 | def add_subdiv_monkey_obj(size, subdiv_viewport_levels, subdiv_render_levels, shade_smooth): 21 | bpy.ops.mesh.primitive_monkey_add(size=size) 22 | 23 | bpy.ops.object.modifier_add(type="SUBSURF") 24 | bpy.context.object.modifiers["Subdivision"].levels = subdiv_viewport_levels 25 | bpy.context.object.modifiers["Subdivision"].render_levels = subdiv_render_levels 26 | 27 | if shade_smooth: 28 | bpy.ops.object.shade_smooth() 29 | 30 | 31 | class MESH_OT_add_subdiv_monkey(bpy.types.Operator): 32 | """Create a new monkey mesh object with a subdivision surf modifier and shaded smooth""" 33 | 34 | bl_idname = "mesh.add_subdiv_monkey" 35 | bl_label = "Add Subdivided Monkey Mesh Object" 36 | bl_options = {"REGISTER", "UNDO"} 37 | 38 | mesh_size: bpy.props.FloatProperty( 39 | name="Size", 40 | default=4.0, 41 | description="The size of the monkey", 42 | ) 43 | 44 | subdiv_viewport_lvl: bpy.props.IntProperty( 45 | name="Subdiv Viewport", 46 | default=1, 47 | min=1, 48 | max=3, 49 | description="The Subdivision Levels applied in the Viewport", 50 | ) 51 | 52 | subdiv_render_lvl: bpy.props.IntProperty( 53 | name="Subdiv Render", 54 | default=3, 55 | min=3, 56 | max=7, 57 | description="The Subdivision Levels applied during the Viewport", 58 | ) 59 | 60 | shade_smooth: bpy.props.BoolProperty( 61 | name="Shade Smooth", 62 | default=True, 63 | description="Apply Smooth Shading to the mesh", 64 | ) 65 | 66 | def execute(self, context): 67 | 68 | add_subdiv_monkey_obj(self.mesh_size, self.subdiv_viewport_lvl, self.subdiv_render_lvl, self.shade_smooth) 69 | 70 | return {"FINISHED"} 71 | 72 | 73 | class VIEW3D_PT_my_custom_panel(bpy.types.Panel): # class naming convention ‘CATEGORY_PT_name’ 74 | 75 | # where to add the panel in the UI 76 | bl_space_type = "VIEW_3D" # 3D Viewport area (find list of values here https://docs.blender.org/api/current/bpy_types_enum_items/space_type_items.html#rna-enum-space-type-items) 77 | bl_region_type = "UI" # Sidebar region (find list of values here https://docs.blender.org/api/current/bpy_types_enum_items/region_type_items.html#rna-enum-region-type-items) 78 | 79 | # add labels 80 | bl_category = "My Custom Panel category" # found in the Sidebar 81 | bl_label = "My Custom Panel label" # found at the top of the Panel 82 | 83 | def draw(self, context): 84 | """define the layout of the panel""" 85 | row = self.layout.row() 86 | row.operator("mesh.primitive_cube_add", text="Add Cube") 87 | row = self.layout.row() 88 | row.operator("mesh.primitive_ico_sphere_add", text="Add Ico Sphere") 89 | row = self.layout.row() 90 | row.operator("object.shade_smooth", text="Shade Smooth") 91 | 92 | self.layout.separator() 93 | 94 | row = self.layout.row() 95 | row.operator("mesh.add_subdiv_monkey", text="Add Subdivided Monkey") 96 | 97 | 98 | # register the panel with Blender 99 | def register(): 100 | bpy.utils.register_class(VIEW3D_PT_my_custom_panel) 101 | bpy.utils.register_class(MESH_OT_add_subdiv_monkey) 102 | 103 | 104 | def unregister(): 105 | bpy.utils.unregister_class(MESH_OT_add_subdiv_monkey) 106 | bpy.utils.unregister_class(VIEW3D_PT_my_custom_panel) 107 | 108 | 109 | if __name__ == "__main__": 110 | register() 111 | -------------------------------------------------------------------------------- /add-ons/simple_custom_panel/simple_custom_panel.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/Qyy_6N3JV3k 3 | """ 4 | 5 | bl_info = { 6 | "name": "My Custom Panel", 7 | "author": "Victor Stepanov", 8 | "version": (0, 0, 1), 9 | "blender": (2, 80, 0), 10 | "location": "3D Viewport > Sidebar > My Custom Panel category", 11 | "description": "My custom operator buttons", 12 | "category": "Development", 13 | } 14 | 15 | # give Python access to Blender's functionality 16 | import bpy 17 | 18 | 19 | class VIEW3D_PT_my_custom_panel(bpy.types.Panel): # class naming convention ‘CATEGORY_PT_name’ 20 | 21 | # where to add the panel in the UI 22 | bl_space_type = "VIEW_3D" # 3D Viewport area (find list of values here https://docs.blender.org/api/current/bpy_types_enum_items/space_type_items.html#rna-enum-space-type-items) 23 | bl_region_type = "UI" # Sidebar region (find list of values here https://docs.blender.org/api/current/bpy_types_enum_items/region_type_items.html#rna-enum-region-type-items) 24 | 25 | bl_category = "My Custom Panel category" # found in the Sidebar 26 | bl_label = "My Custom Panel label" # found at the top of the Panel 27 | 28 | def draw(self, context): 29 | """define the layout of the panel""" 30 | row = self.layout.row() 31 | row.operator("mesh.primitive_cube_add", text="Add Cube") 32 | row = self.layout.row() 33 | row.operator("mesh.primitive_ico_sphere_add", text="Add Ico Sphere") 34 | row = self.layout.row() 35 | row.operator("object.shade_smooth", text="Shade Smooth") 36 | 37 | 38 | def register(): 39 | bpy.utils.register_class(VIEW3D_PT_my_custom_panel) 40 | 41 | 42 | def unregister(): 43 | bpy.utils.unregister_class(VIEW3D_PT_my_custom_panel) 44 | 45 | 46 | if __name__ == "__main__": 47 | register() 48 | -------------------------------------------------------------------------------- /assets/images/color_slices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/color_slices.png -------------------------------------------------------------------------------- /assets/images/cube_loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/cube_loop.png -------------------------------------------------------------------------------- /assets/images/floret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/floret.png -------------------------------------------------------------------------------- /assets/images/flow_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/flow_out.png -------------------------------------------------------------------------------- /assets/images/import_into_sequence_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/import_into_sequence_editor.png -------------------------------------------------------------------------------- /assets/images/in_or_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/in_or_out.png -------------------------------------------------------------------------------- /assets/images/loop_of_rings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/loop_of_rings.png -------------------------------------------------------------------------------- /assets/images/shapeshifting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/shapeshifting.png -------------------------------------------------------------------------------- /assets/images/video_grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/video_grid.png -------------------------------------------------------------------------------- /assets/images/weave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CGArtPython/blender_plus_python/ccea1b77faa4129221bf4a161242165f5a4d7833/assets/images/weave.png -------------------------------------------------------------------------------- /automation/apply_color_palettes/apply_color_palettes_done.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script is used to render color palettes in Blender. It loads color palettes from a JSON file, selects a random color palette, and updates the colors of materials and nodes in the Blender scene based on the selected palette. The scene is then rendered and saved as a PNG image. 3 | 4 | The script contains several helper functions for tasks such as setting the random seed, converting hex color strings to RGBA values, selecting random color palettes, choosing random colors from a palette, and updating colors in the scene. 5 | 6 | To use the script, simply run it. By default, it will render all the color palettes loaded from the JSON file. You can also specify a specific color palette index and random seed to render a single palette. 7 | 8 | Note: This script assumes that the JSON file containing the color palettes is located at the path specified in the `load_color_palettes` function. 9 | """ 10 | 11 | import json 12 | import math 13 | import pathlib 14 | import random 15 | import time 16 | 17 | import bpy 18 | 19 | ################################################################ 20 | # helper functions BEGIN 21 | ################################################################ 22 | 23 | 24 | def time_seed(): 25 | """ 26 | Sets the random seed based on the time 27 | and copies the seed into the clipboard 28 | 29 | Returns: 30 | - seed (int): The random seed based on the current time. 31 | """ 32 | seed = int(time.time()) 33 | print(f"seed: {seed}") 34 | random.seed(seed) 35 | 36 | # add the seed value to your clipboard 37 | bpy.context.window_manager.clipboard = str(seed) 38 | 39 | return seed 40 | 41 | 42 | def hex_color_str_to_rgba(hex_color: str): 43 | """ 44 | Converting from a color in the form of a hex triplet string (en.wikipedia.org/wiki/Web_colors#Hex_triplet) 45 | to a Linear RGB with an Alpha of 1.0 46 | 47 | Args: 48 | - hex_color (str): The hex color string in the format "#RRGGBB" or "RRGGBB" 49 | 50 | Returns: 51 | - rgba_color (tuple): The Linear RGB color with an Alpha of 1.0 52 | """ 53 | # remove the leading '#' symbol if present 54 | if hex_color.startswith("#"): 55 | hex_color = hex_color[1:] 56 | 57 | assert len(hex_color) == 6, "RRGGBB is the supported hex color format" 58 | 59 | # extracting the Red color component - RRxxxx 60 | red = int(hex_color[:2], 16) 61 | # dividing by 255 to get a number between 0.0 and 1.0 62 | srgb_red = red / 255 63 | linear_red = convert_srgb_to_linear_rgb(srgb_red) 64 | 65 | # extracting the Green color component - xxGGxx 66 | green = int(hex_color[2:4], 16) 67 | # dividing by 255 to get a number between 0.0 and 1.0 68 | srgb_green = green / 255 69 | linear_green = convert_srgb_to_linear_rgb(srgb_green) 70 | 71 | # extracting the Blue color component - xxxxBB 72 | blue = int(hex_color[4:6], 16) 73 | # dividing by 255 to get a number between 0.0 and 1.0 74 | srgb_blue = blue / 255 75 | linear_blue = convert_srgb_to_linear_rgb(srgb_blue) 76 | 77 | alpha = 1.0 78 | return tuple([linear_red, linear_green, linear_blue, alpha]) 79 | 80 | 81 | def convert_srgb_to_linear_rgb(srgb_color_component): 82 | """ 83 | Converting from sRGB to Linear RGB 84 | based on https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ 85 | 86 | Args: 87 | - srgb_color_component (float): The sRGB color component value 88 | 89 | Returns: 90 | - linear_color_component (float): The linear RGB color component value 91 | """ 92 | if srgb_color_component <= 0.04045: 93 | linear_color_component = srgb_color_component / 12.92 94 | else: 95 | linear_color_component = math.pow((srgb_color_component + 0.055) / 1.055, 2.4) 96 | 97 | return linear_color_component 98 | 99 | 100 | def choose_random_color(palette, exclude_colors=None): 101 | """ 102 | Chooses a random color from the given palette, excluding the specified colors if provided. 103 | 104 | Args: 105 | - palette (list): The color palette to choose from. 106 | - exclude_colors (list, optional): The colors to exclude from the selection. 107 | 108 | Returns: 109 | - color (str): The randomly selected color. 110 | """ 111 | if not exclude_colors: 112 | return random.choice(palette) 113 | 114 | while True: 115 | color = random.choice(palette) 116 | if color not in exclude_colors: 117 | return color 118 | 119 | 120 | ################################################################ 121 | # helper functions END 122 | ################################################################ 123 | 124 | 125 | def load_color_palettes(): 126 | """ 127 | Loads the color palettes from a JSON file. 128 | 129 | Returns: 130 | - color_palettes (list): The list of color palettes loaded from the JSON file. 131 | """ 132 | # https://github.com/CGArtPython/get_color_palettes_py/blob/main/palettes/1000_five_color_palettes.json 133 | path = pathlib.Path.home() / "tmp" / "1000_five_color_palettes.json" 134 | with open(path, "r") as color_palette: 135 | color_palettes = json.loads(color_palette.read()) 136 | 137 | return color_palettes 138 | 139 | 140 | def setup_scene(palette_index, seed=0): 141 | """ 142 | Sets up the scene for rendering with the specified palette index and seed. 143 | 144 | Args: 145 | - palette_index (int): The index of the color palette to use. 146 | - seed (float, optional): The random seed to use. If not provided, a new seed will be generated based on the current time. 147 | """ 148 | if seed: 149 | random.seed(seed) 150 | else: 151 | seed = time_seed() 152 | 153 | project_name = "applying_1k_color_palettes" 154 | 155 | render_dir_path = pathlib.Path.home() / project_name / f"palette_{palette_index}_seed_{seed}.png" 156 | render_dir_path.parent.mkdir(parents=True, exist_ok=True) 157 | 158 | bpy.context.scene.render.image_settings.file_format = "PNG" 159 | bpy.context.scene.render.filepath = str(render_dir_path) 160 | 161 | 162 | def prepare_and_render_scene(palette, palette_index, seed=None): 163 | """ 164 | Prepares and renders the scene with the specified palette and index. 165 | 166 | Args: 167 | - palette (list): The color palette to use for updating the colors. 168 | - palette_index (int): The index of the color palette. 169 | - seed (float, optional): The random seed to use. If not provided, a new seed will be generated based on the current time. 170 | """ 171 | setup_scene(palette_index, seed) 172 | update_colors(palette) 173 | bpy.ops.render.render(write_still=True) 174 | 175 | 176 | def render_all_palettes(palettes): 177 | """ 178 | Renders all the color palettes. 179 | 180 | Args: 181 | palettes (list): A list of color palettes to be rendered. 182 | """ 183 | # make sure we are using the EEVEE render engine for faster rendering 184 | bpy.context.scene.render.engine = "BLENDER_EEVEE" 185 | 186 | start_time = time.time() 187 | for palette_index, palette in enumerate(palettes): 188 | prepare_and_render_scene(palette, palette_index) 189 | 190 | # remove the following line to render all the palettes 191 | break 192 | 193 | end_time = time.time() 194 | execution_time = end_time - start_time 195 | print(f"Execution time: {execution_time} seconds") 196 | 197 | 198 | def update_colors(palette): 199 | """ 200 | Updates the colors of the materials and nodes in the Blender scene based on the given palette. 201 | 202 | Args: 203 | - palette (list): The color palette to use for updating the colors. 204 | """ 205 | random.shuffle(palette) 206 | 207 | palette = [hex_color_str_to_rgba(hex_color) for hex_color in palette] 208 | 209 | backdrop_bsdf_node = bpy.data.materials["backdrop"].node_tree.nodes["Principled BSDF"] 210 | backdrop_color = choose_random_color(palette) 211 | backdrop_bsdf_node.inputs["Base Color"].default_value = backdrop_color 212 | 213 | upper_platform_bsdf_node = bpy.data.materials["upper_platform"].node_tree.nodes["Principled BSDF"] 214 | platform_color = choose_random_color(palette, exclude_colors=[backdrop_color]) 215 | upper_platform_bsdf_node.inputs["Base Color"].default_value = platform_color 216 | 217 | pillar_bsdf_node = bpy.data.materials["pillar"].node_tree.nodes["Principled BSDF"] 218 | pillar_color = choose_random_color(palette, exclude_colors=[backdrop_color, platform_color]) 219 | pillar_bsdf_node.inputs["Base Color"].default_value = pillar_color 220 | 221 | icing_bsdf_node = bpy.data.materials["icing"].node_tree.nodes["Principled BSDF"] 222 | icing_color = choose_random_color(palette, exclude_colors=[backdrop_color]) 223 | icing_bsdf_node.inputs["Base Color"].default_value = pillar_color 224 | 225 | color_ramp = bpy.data.materials["sprinkles"].node_tree.nodes["ColorRamp"].color_ramp 226 | color_ramp.elements[0].color = choose_random_color(palette, exclude_colors=[icing_color]) 227 | color_ramp.elements[1].color = choose_random_color(palette, exclude_colors=[icing_color]) 228 | color_ramp.elements[2].color = choose_random_color(palette, exclude_colors=[icing_color]) 229 | color_ramp.elements[3].color = choose_random_color(palette, exclude_colors=[icing_color]) 230 | 231 | 232 | def main(): 233 | """ 234 | The main entry point of the script. 235 | """ 236 | palettes = load_color_palettes() 237 | 238 | palette_index = None 239 | seed = None 240 | 241 | if palette_index is not None and seed is not None: 242 | bpy.context.scene.render.engine = "CYCLES" 243 | selected_palette = palettes[palette_index] 244 | prepare_and_render_scene(selected_palette, palette_index, seed) 245 | return 246 | 247 | render_all_palettes(palettes) 248 | 249 | 250 | if __name__ == "__main__": 251 | main() 252 | -------------------------------------------------------------------------------- /automation/apply_color_palettes/apply_color_palettes_start.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script is used to render color palettes in Blender. It loads color palettes from a JSON file, selects a random color palette, and updates the colors of materials and nodes in the Blender scene based on the selected palette. The scene is then rendered and saved as a PNG image. 3 | 4 | The script contains several helper functions for tasks such as setting the random seed, converting hex color strings to RGBA values, selecting random color palettes, choosing random colors from a palette, and updating colors in the scene. 5 | 6 | To use the script, simply run it. By default, it will render all the color palettes loaded from the JSON file. You can also specify a specific color palette index and random seed to render a single palette. 7 | 8 | Note: This script assumes that the JSON file containing the color palettes is located at the path specified in the `load_color_palettes` function. 9 | """ 10 | 11 | import json 12 | import math 13 | import pathlib 14 | import random 15 | import time 16 | 17 | import bpy 18 | 19 | ################################################################ 20 | # helper functions BEGIN 21 | ################################################################ 22 | 23 | 24 | def time_seed(): 25 | """ 26 | Sets the random seed based on the time 27 | and copies the seed into the clipboard 28 | 29 | Returns: 30 | - seed (int): The random seed based on the current time. 31 | """ 32 | seed = int(time.time()) 33 | print(f"seed: {seed}") 34 | random.seed(seed) 35 | 36 | # add the seed value to your clipboard 37 | bpy.context.window_manager.clipboard = str(seed) 38 | 39 | return seed 40 | 41 | 42 | def hex_color_str_to_rgba(hex_color: str): 43 | """ 44 | Converting from a color in the form of a hex triplet string (en.wikipedia.org/wiki/Web_colors#Hex_triplet) 45 | to a Linear RGB with an Alpha of 1.0 46 | 47 | Args: 48 | - hex_color (str): The hex color string in the format "#RRGGBB" or "RRGGBB" 49 | 50 | Returns: 51 | - rgba_color (tuple): The Linear RGB color with an Alpha of 1.0 52 | """ 53 | # remove the leading '#' symbol if present 54 | if hex_color.startswith("#"): 55 | hex_color = hex_color[1:] 56 | 57 | assert len(hex_color) == 6, "RRGGBB is the supported hex color format" 58 | 59 | # extracting the Red color component - RRxxxx 60 | red = int(hex_color[:2], 16) 61 | # dividing by 255 to get a number between 0.0 and 1.0 62 | srgb_red = red / 255 63 | linear_red = convert_srgb_to_linear_rgb(srgb_red) 64 | 65 | # extracting the Green color component - xxGGxx 66 | green = int(hex_color[2:4], 16) 67 | # dividing by 255 to get a number between 0.0 and 1.0 68 | srgb_green = green / 255 69 | linear_green = convert_srgb_to_linear_rgb(srgb_green) 70 | 71 | # extracting the Blue color component - xxxxBB 72 | blue = int(hex_color[4:6], 16) 73 | # dividing by 255 to get a number between 0.0 and 1.0 74 | srgb_blue = blue / 255 75 | linear_blue = convert_srgb_to_linear_rgb(srgb_blue) 76 | 77 | alpha = 1.0 78 | return tuple([linear_red, linear_green, linear_blue, alpha]) 79 | 80 | 81 | def convert_srgb_to_linear_rgb(srgb_color_component): 82 | """ 83 | Converting from sRGB to Linear RGB 84 | based on https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ 85 | 86 | Args: 87 | - srgb_color_component (float): The sRGB color component value 88 | 89 | Returns: 90 | - linear_color_component (float): The linear RGB color component value 91 | """ 92 | if srgb_color_component <= 0.04045: 93 | linear_color_component = srgb_color_component / 12.92 94 | else: 95 | linear_color_component = math.pow((srgb_color_component + 0.055) / 1.055, 2.4) 96 | 97 | return linear_color_component 98 | 99 | 100 | def select_random_color_palette(context): 101 | """ 102 | Selects a random color palette from the available color palettes. 103 | 104 | Args: 105 | - context (dict): The context containing the available color palettes. 106 | 107 | Returns: 108 | - random_palette (list): The randomly selected color palette. 109 | """ 110 | random_palette = random.choice(context["color_palettes"]) 111 | print(f"Random palette: {random_palette}") 112 | return random_palette 113 | 114 | 115 | def choose_random_color(palette, exclude_colors=None): 116 | """ 117 | Chooses a random color from the given palette, excluding the specified colors if provided. 118 | 119 | Args: 120 | - palette (list): The color palette to choose from. 121 | - exclude_colors (list, optional): The colors to exclude from the selection. 122 | 123 | Returns: 124 | - color (str): The randomly selected color. 125 | """ 126 | if not exclude_colors: 127 | return random.choice(palette) 128 | 129 | while True: 130 | color = random.choice(palette) 131 | if color not in exclude_colors: 132 | return color 133 | 134 | 135 | ################################################################ 136 | # helper functions END 137 | ################################################################ 138 | 139 | 140 | def load_color_palettes(): 141 | """ 142 | Loads the color palettes from a JSON file. 143 | 144 | Returns: 145 | - color_palettes (list): The list of color palettes loaded from the JSON file. 146 | """ 147 | # https://github.com/CGArtPython/get_color_palettes_py/blob/main/palettes/1000_five_color_palettes.json 148 | path = pathlib.Path.home() / "tmp" / "1000_five_color_palettes.json" 149 | with open(path, "r") as color_palette: 150 | color_palettes = json.loads(color_palette.read()) 151 | 152 | return color_palettes 153 | 154 | 155 | def setup_scene(palette_index, seed=0): 156 | """ 157 | Sets up the scene for rendering with the specified palette index and seed. 158 | 159 | Args: 160 | - palette_index (int): The index of the color palette to use. 161 | - seed (float, optional): The random seed to use. If not provided, a new seed will be generated based on the current time. 162 | """ 163 | if seed: 164 | random.seed(seed) 165 | else: 166 | seed = time_seed() 167 | 168 | project_name = "applying_1k_color_palettes" 169 | 170 | render_dir_path = pathlib.Path.home() / project_name / f"palette_{palette_index}_seed_{seed}.png" 171 | render_dir_path.parent.mkdir(parents=True, exist_ok=True) 172 | 173 | bpy.context.scene.render.image_settings.file_format = "PNG" 174 | bpy.context.scene.render.filepath = str(render_dir_path) 175 | 176 | 177 | def prepare_and_render_scene(palette, palette_index, seed=None): 178 | """ 179 | Prepares and renders the scene with the specified palette and index. 180 | 181 | Args: 182 | - palette (list): The color palette to use for updating the colors. 183 | - palette_index (int): The index of the color palette. 184 | - seed (float, optional): The random seed to use. If not provided, a new seed will be generated based on the current time. 185 | """ 186 | setup_scene(palette_index, seed) 187 | update_colors(palette) 188 | bpy.ops.render.render(write_still=True) 189 | 190 | 191 | def render_all_palettes(palettes): 192 | """ 193 | Renders all the color palettes. 194 | 195 | Args: 196 | palettes (list): A list of color palettes to be rendered. 197 | """ 198 | # make sure we are using the EEVEE render engine for faster rendering 199 | bpy.context.scene.render.engine = "BLENDER_EEVEE" 200 | 201 | start_time = time.time() 202 | for palette_index, palette in enumerate(palettes): 203 | prepare_and_render_scene(palette, palette_index) 204 | 205 | # remove the following line to render all the palettes 206 | break 207 | 208 | end_time = time.time() 209 | execution_time = end_time - start_time 210 | print(f"Execution time: {execution_time} seconds") 211 | 212 | 213 | def update_colors(palette): 214 | """ 215 | Updates the colors of the materials and nodes in the Blender scene based on the given palette. 216 | 217 | Args: 218 | - palette (list): The color palette to use for updating the colors. 219 | """ 220 | random.shuffle(palette) 221 | 222 | palette = [hex_color_str_to_rgba(hex_color) for hex_color in palette] 223 | 224 | # YOUR CODE HERE 225 | 226 | 227 | def main(): 228 | """ 229 | The main entry point of the script. 230 | """ 231 | palettes = load_color_palettes() 232 | 233 | palette_index = None 234 | seed = None 235 | 236 | if palette_index is not None and seed is not None: 237 | bpy.context.scene.render.engine = "CYCLES" 238 | selected_palette = palettes[palette_index] 239 | prepare_and_render_scene(selected_palette, palette_index, seed) 240 | return 241 | 242 | render_all_palettes(palettes) 243 | 244 | 245 | if __name__ == "__main__": 246 | main() 247 | -------------------------------------------------------------------------------- /automation/basic_360_turntable_animation/basic_360_turntable_animation_done_bpybb.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://www.youtube.com/watch?v=BSsjSj0iOaE 3 | """ 4 | import datetime 5 | import functools 6 | import pathlib 7 | 8 | import mathutils 9 | import bpy 10 | 11 | # you need to install the bpybb Python package (https://www.youtube.com/watch?v=_irmuKXjhS0) 12 | from bpybb.animate import animate_360_rotation 13 | from bpybb.empty import add_ctrl_empty 14 | from bpybb.output import set_1080p_render_res 15 | from bpybb.utils import clean_scene, active_object, make_active, Axis 16 | 17 | ################################################################ 18 | # region helper functions BEGIN 19 | ################################################################ 20 | 21 | 22 | def set_scene_props(fps, loop_seconds): 23 | """ 24 | Set scene properties 25 | """ 26 | frame_count = fps * loop_seconds 27 | 28 | scene = bpy.context.scene 29 | scene.frame_end = frame_count 30 | 31 | # set the world background to black 32 | world = bpy.data.worlds["World"] 33 | if "Background" in world.node_tree.nodes: 34 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 35 | 36 | scene.render.fps = fps 37 | 38 | scene.frame_current = 1 39 | scene.frame_start = 1 40 | 41 | scene.render.engine = "CYCLES" 42 | 43 | # Use the GPU to render 44 | scene.cycles.device = "GPU" 45 | 46 | # Use the CPU to render 47 | # scene.cycles.device = "CPU" 48 | 49 | scene.cycles.samples = 300 50 | 51 | scene.view_settings.look = "Very High Contrast" 52 | 53 | set_1080p_render_res() 54 | 55 | 56 | def remove_libraries(): 57 | bpy.data.batch_remove(bpy.data.libraries) 58 | 59 | 60 | @functools.cache 61 | def get_script_path(): 62 | # check if we are running from the Text Editor 63 | if bpy.context.space_data != None and bpy.context.space_data.type == "TEXT_EDITOR": 64 | print("bpy.context.space_data script_path") 65 | script_path = bpy.context.space_data.text.filepath 66 | if not script_path: 67 | print("ERROR: Can't get the script file folder path, because you haven't saved the script file.") 68 | else: 69 | print("__file__ script_path") 70 | script_path = __file__ 71 | 72 | return script_path 73 | 74 | 75 | @functools.cache 76 | def get_script_folder_path(): 77 | script_path = get_script_path() 78 | return pathlib.Path(script_path).resolve().parent 79 | 80 | 81 | def scene_setup(): 82 | fps = 30 83 | loop_seconds = 12 84 | frame_count = fps * loop_seconds 85 | 86 | clean_scene() 87 | remove_libraries() 88 | 89 | for lib in bpy.data.libraries: 90 | bpy.data.batch_remove(ids=(lib,)) 91 | 92 | set_scene_props(fps, loop_seconds) 93 | 94 | context = { 95 | "frame_count": frame_count, 96 | "loop_frame_count": frame_count + 1, 97 | } 98 | 99 | return context 100 | 101 | 102 | def create_light_rig(light_count, light_type="AREA", rig_radius=2.0, light_radius=1.0, energy=100): 103 | bpy.ops.mesh.primitive_circle_add(vertices=light_count, radius=rig_radius) 104 | rig_obj = active_object() 105 | 106 | empty = add_ctrl_empty(name="empty.tracker-target.lights") 107 | 108 | for i in range(light_count): 109 | loc = rig_obj.data.vertices[i].co 110 | 111 | bpy.ops.object.light_add(type=light_type, radius=light_radius, location=loc) 112 | light = active_object() 113 | light.data.energy = energy 114 | light.parent = rig_obj 115 | 116 | bpy.ops.object.constraint_add(type="TRACK_TO") 117 | light.constraints["Track To"].target = empty 118 | 119 | return rig_obj, empty 120 | 121 | 122 | def get_working_directory_path(): 123 | """This function provides the folder path that the script will be operating from. 124 | There are examples of paths that you can use, just uncomment the path that you want to use. 125 | """ 126 | # folder_path = pathlib.Path().home() / "tmp" 127 | 128 | folder_path = get_script_folder_path() 129 | 130 | # Examples of other folder paths 131 | ## Windows 132 | ### folder_path = pathlib.Path(r"E:\my_projects\project_123") 133 | ## Linux/macOS 134 | ### folder_path = pathlib.Path().home() / "my_projects" / "project_123" 135 | 136 | return folder_path 137 | 138 | 139 | ################################################################ 140 | # endregion helper functions END 141 | ################################################################ 142 | 143 | 144 | def get_list_of_blend_files(path): 145 | 146 | blend_files = [] 147 | for blend_file_path in pathlib.Path(path).rglob("*.blend"): 148 | blend_files.append(blend_file_path) 149 | 150 | return blend_files 151 | 152 | 153 | def link_objects(blend_file_path, with_name=None): 154 | 155 | # link the blender file objects into the current blender file 156 | with bpy.data.libraries.load(blend_file_path, link=True) as (data_from, data_to): 157 | data_to.objects = data_from.objects 158 | 159 | scene = bpy.context.scene 160 | 161 | linked_objects = [] 162 | 163 | # link the objects into the scene collection 164 | for obj in data_to.objects: 165 | if obj is None: 166 | continue 167 | 168 | if with_name and with_name not in obj.name: 169 | continue 170 | 171 | scene.collection.objects.link(obj) 172 | linked_objects.append(obj) 173 | 174 | return linked_objects 175 | 176 | 177 | def create_floor(): 178 | path = pathlib.Path.home() / "tmp" / "grid_floor.blend" 179 | 180 | if path.exists(): 181 | link_objects(str(path)) 182 | else: 183 | bpy.ops.mesh.primitive_plane_add(size=100) 184 | floor = active_object() 185 | 186 | material = bpy.data.materials.new(name="floor_material") 187 | material.diffuse_color = (0.0003, 0.0074, 0.0193, 1.0) 188 | floor.data.materials.append(material) 189 | 190 | 191 | def prepare_scene(frame_count): 192 | 193 | create_floor() 194 | 195 | light_rig_obj, _ = create_light_rig(light_count=3) 196 | 197 | bpy.ops.object.empty_add() 198 | focus_empty = bpy.context.active_object 199 | animate_360_rotation(Axis.Z, frame_count) 200 | 201 | bpy.ops.object.camera_add() 202 | camera_obj = bpy.context.active_object 203 | bpy.context.scene.camera = camera_obj 204 | 205 | bpy.ops.object.constraint_add(type="TRACK_TO") 206 | bpy.context.object.constraints["Track To"].target = focus_empty 207 | camera_obj.parent = focus_empty 208 | 209 | return camera_obj, focus_empty, light_rig_obj 210 | 211 | 212 | def get_object_center(target_obj): 213 | bound_box_coord_sum = mathutils.Vector() 214 | for bound_box_coord in target_obj.bound_box: 215 | bound_box_coord_sum += mathutils.Vector(bound_box_coord) 216 | 217 | local_obj_center = bound_box_coord_sum / len(target_obj.bound_box) 218 | return target_obj.matrix_world @ local_obj_center 219 | 220 | 221 | def focus_camera_on_target_obj(camera_obj, target_obj): 222 | make_active(target_obj) 223 | 224 | bpy.ops.view3d.camera_to_view_selected() 225 | 226 | # zoom out a bit to get some space between the edge of the camera and the object 227 | camera_obj.location *= 1.5 228 | 229 | 230 | def update_scene(target_obj, focus_empty, camera_obj, light_rig_obj): 231 | 232 | obj_center = get_object_center(target_obj) 233 | 234 | focus_empty.location = obj_center 235 | 236 | height = target_obj.dimensions.z * 2 237 | camera_obj.location = (height, height, height) 238 | 239 | focus_camera_on_target_obj(camera_obj, target_obj) 240 | 241 | light_rig_obj.location.z = camera_obj.location.z 242 | 243 | 244 | def run_turntable_render(model_name, output_folder_path): 245 | time_stamp = datetime.datetime.now().strftime("%H-%M-%S") 246 | 247 | scene = bpy.context.scene 248 | 249 | scene.render.image_settings.file_format = "FFMPEG" 250 | scene.render.ffmpeg.format = "MPEG4" 251 | scene.render.filepath = str(output_folder_path / f"{model_name}_turntable_{time_stamp}.mp4") 252 | 253 | bpy.ops.render.render(animation=True) 254 | 255 | 256 | def unlink_objects(objects): 257 | scene = bpy.context.scene 258 | 259 | # unlink objects from the 260 | for obj in objects: 261 | if obj is not None: 262 | scene.collection.objects.unlink(obj) 263 | 264 | 265 | def render_turntable_models(context, blend_files): 266 | 267 | camera_obj, focus_empty, light_rig_obj = prepare_scene(context["loop_frame_count"]) 268 | 269 | # we will be looking for models with this text in their name 270 | target_substr_name = "target" 271 | for blend_file in blend_files: 272 | print(f"processing {blend_file}") 273 | objects = link_objects(str(blend_file), with_name=target_substr_name) 274 | 275 | if not objects: 276 | print(f"didn't find any model with '{target_substr_name}' in it's name in {blend_file}") 277 | continue 278 | 279 | target_obj = objects[0] 280 | 281 | update_scene(target_obj, focus_empty, camera_obj, light_rig_obj) 282 | 283 | print(f"rendering turntable {blend_file}") 284 | output_folder_path = pathlib.Path(blend_file).parent 285 | run_turntable_render(target_obj.name, output_folder_path) 286 | 287 | unlink_objects(objects) 288 | 289 | 290 | def main(): 291 | """ 292 | A script that finds all blend files under a path, 293 | links the target models into the current scene, and renders a turntable loop .mp4 294 | """ 295 | context = scene_setup() 296 | 297 | # example of a path in your home folder 298 | models_folder_path = get_working_directory_path() / "models" 299 | 300 | blend_files = get_list_of_blend_files(models_folder_path) 301 | 302 | render_turntable_models(context, blend_files) 303 | 304 | 305 | if __name__ == "__main__": 306 | main() 307 | -------------------------------------------------------------------------------- /automation/basic_360_turntable_animation/basic_360_turntable_animation_start.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://www.youtube.com/watch?v=BSsjSj0iOaE 3 | """ 4 | import datetime 5 | import functools 6 | import math 7 | import pathlib 8 | 9 | import mathutils 10 | import bpy 11 | 12 | ################################################################ 13 | # region helper functions BEGIN 14 | ################################################################ 15 | 16 | 17 | def purge_orphans(): 18 | """ 19 | Remove all orphan data blocks 20 | 21 | see this from more info: 22 | https://youtu.be/3rNqVPtbhzc?t=149 23 | """ 24 | if bpy.app.version >= (3, 0, 0): 25 | # run this only for Blender versions 3.0 and higher 26 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 27 | else: 28 | # run this only for Blender versions lower than 3.0 29 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 30 | result = bpy.ops.outliner.orphans_purge() 31 | if result.pop() != "CANCELLED": 32 | purge_orphans() 33 | 34 | 35 | def clean_scene(): 36 | """ 37 | Removing all of the objects, collection, materials, particles, 38 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 39 | 40 | Checkout this video explanation with example 41 | 42 | "How to clean the scene with Python in Blender (with examples)" 43 | https://youtu.be/3rNqVPtbhzc 44 | """ 45 | # make sure the active object is not in Edit Mode 46 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 47 | bpy.ops.object.editmode_toggle() 48 | 49 | # make sure non of the objects are hidden from the viewport, selection, or disabled 50 | for obj in bpy.data.objects: 51 | obj.hide_set(False) 52 | obj.hide_select = False 53 | obj.hide_viewport = False 54 | 55 | # select all the object and delete them (just like pressing A + X + D in the viewport) 56 | bpy.ops.object.select_all(action="SELECT") 57 | bpy.ops.object.delete() 58 | 59 | # find all the collections and remove them 60 | collection_names = [col.name for col in bpy.data.collections] 61 | for name in collection_names: 62 | bpy.data.collections.remove(bpy.data.collections[name]) 63 | 64 | # in the case when you modify the world shader 65 | # delete and recreate the world object 66 | world_names = [world.name for world in bpy.data.worlds] 67 | for name in world_names: 68 | bpy.data.worlds.remove(bpy.data.worlds[name]) 69 | # create a new world data block 70 | bpy.ops.world.new() 71 | bpy.context.scene.world = bpy.data.worlds["World"] 72 | 73 | purge_orphans() 74 | 75 | 76 | def active_object(): 77 | """ 78 | returns the currently active object 79 | """ 80 | return bpy.context.active_object 81 | 82 | 83 | def add_ctrl_empty(name=None): 84 | 85 | bpy.ops.object.empty_add(type="PLAIN_AXES") 86 | empty_ctrl = active_object() 87 | 88 | if name: 89 | empty_ctrl.name = f"empty.{name}" 90 | else: 91 | empty_ctrl.name = "empty.cntrl" 92 | 93 | return empty_ctrl 94 | 95 | 96 | def make_active(obj): 97 | bpy.ops.object.select_all(action="DESELECT") 98 | obj.select_set(True) 99 | bpy.context.view_layer.objects.active = obj 100 | 101 | 102 | def set_fcurve_extrapolation_to_linear(obj=None): 103 | """ 104 | Loops over all the fcurves of an action 105 | and sets the extrapolation to "LINEAR". 106 | """ 107 | if obj is None: 108 | obj = active_object() 109 | 110 | for fcurve in obj.animation_data.action.fcurves: 111 | fcurve.extrapolation = "LINEAR" 112 | 113 | 114 | class Axis: 115 | X = 0 116 | Y = 1 117 | Z = 2 118 | 119 | 120 | def animate_360_rotation(axis_index, last_frame, obj=None, clockwise=False, linear=True, start_frame=1): 121 | animate_rotation(360, axis_index, last_frame, obj, clockwise, linear, start_frame) 122 | 123 | 124 | def animate_rotation(angle, axis_index, last_frame, obj=None, clockwise=False, linear=True, start_frame=1): 125 | if not obj: 126 | obj = active_object() 127 | frame = start_frame 128 | obj.keyframe_insert("rotation_euler", index=axis_index, frame=frame) 129 | 130 | if clockwise: 131 | angle_offset = -angle 132 | else: 133 | angle_offset = angle 134 | frame = last_frame 135 | obj.rotation_euler[axis_index] = math.radians(angle_offset) + obj.rotation_euler[axis_index] 136 | obj.keyframe_insert("rotation_euler", index=axis_index, frame=frame) 137 | 138 | if linear: 139 | set_fcurve_extrapolation_to_linear() 140 | 141 | 142 | def set_1080p_render_res(): 143 | """ 144 | Set the resolution of the rendered image to 1080p 145 | """ 146 | bpy.context.scene.render.resolution_x = 1920 147 | bpy.context.scene.render.resolution_y = 1080 148 | 149 | 150 | # bpybb end 151 | 152 | 153 | def set_scene_props(fps, loop_seconds): 154 | """ 155 | Set scene properties 156 | """ 157 | frame_count = fps * loop_seconds 158 | 159 | scene = bpy.context.scene 160 | scene.frame_end = frame_count 161 | 162 | # set the world background to black 163 | world = bpy.data.worlds["World"] 164 | if "Background" in world.node_tree.nodes: 165 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 166 | 167 | scene.render.fps = fps 168 | 169 | scene.frame_current = 1 170 | scene.frame_start = 1 171 | 172 | scene.render.engine = "CYCLES" 173 | 174 | # Use the GPU to render 175 | scene.cycles.device = "GPU" 176 | 177 | # Use the CPU to render 178 | # scene.cycles.device = "CPU" 179 | 180 | scene.cycles.samples = 300 181 | 182 | scene.view_settings.look = "Very High Contrast" 183 | 184 | set_1080p_render_res() 185 | 186 | 187 | def remove_libraries(): 188 | bpy.data.batch_remove(bpy.data.libraries) 189 | 190 | 191 | @functools.cache 192 | def get_script_path(): 193 | # check if we are running from the Text Editor 194 | if bpy.context.space_data != None and bpy.context.space_data.type == "TEXT_EDITOR": 195 | print("bpy.context.space_data script_path") 196 | script_path = bpy.context.space_data.text.filepath 197 | if not script_path: 198 | print("ERROR: Can't get the script file folder path, because you haven't saved the script file.") 199 | else: 200 | print("__file__ script_path") 201 | script_path = __file__ 202 | 203 | return script_path 204 | 205 | 206 | @functools.cache 207 | def get_script_folder_path(): 208 | script_path = get_script_path() 209 | return pathlib.Path(script_path).resolve().parent 210 | 211 | 212 | def scene_setup(): 213 | fps = 30 214 | loop_seconds = 12 215 | frame_count = fps * loop_seconds 216 | 217 | clean_scene() 218 | remove_libraries() 219 | 220 | for lib in bpy.data.libraries: 221 | bpy.data.batch_remove(ids=(lib,)) 222 | 223 | set_scene_props(fps, loop_seconds) 224 | 225 | context = { 226 | "frame_count": frame_count, 227 | "loop_frame_count": frame_count + 1, 228 | } 229 | 230 | return context 231 | 232 | 233 | def create_light_rig(light_count, light_type="AREA", rig_radius=2.0, light_radius=1.0, energy=100): 234 | bpy.ops.mesh.primitive_circle_add(vertices=light_count, radius=rig_radius) 235 | rig_obj = active_object() 236 | 237 | empty = add_ctrl_empty(name="empty.tracker-target.lights") 238 | 239 | for i in range(light_count): 240 | loc = rig_obj.data.vertices[i].co 241 | 242 | bpy.ops.object.light_add(type=light_type, radius=light_radius, location=loc) 243 | light = active_object() 244 | light.data.energy = energy 245 | light.parent = rig_obj 246 | 247 | bpy.ops.object.constraint_add(type="TRACK_TO") 248 | light.constraints["Track To"].target = empty 249 | 250 | return rig_obj, empty 251 | 252 | 253 | def get_working_directory_path(): 254 | """This function provides the folder path that the script will be operating from. 255 | There are examples of paths that you can use, just uncomment the path that you want to use. 256 | """ 257 | # folder_path = pathlib.Path().home() / "tmp" 258 | 259 | folder_path = get_script_folder_path() 260 | 261 | # Examples of other folder paths 262 | ## Windows 263 | ### folder_path = pathlib.Path(r"E:\my_projects\project_123") 264 | ## Linux/macOS 265 | ### folder_path = pathlib.Path().home() / "my_projects" / "project_123" 266 | 267 | return folder_path 268 | 269 | 270 | ################################################################ 271 | # endregion helper functions END 272 | ################################################################ 273 | 274 | 275 | def main(): 276 | """ 277 | A script that finds all blend files under a path, 278 | links the target models into the current scene, and renders a turntable loop .mp4 279 | """ 280 | context = scene_setup() 281 | 282 | # example of a path in your home folder 283 | models_folder_path = get_working_directory_path() / "models" 284 | 285 | 286 | if __name__ == "__main__": 287 | main() 288 | -------------------------------------------------------------------------------- /bpybb/set_up_world_sun_light/set_up_world_sun_light_end.py: -------------------------------------------------------------------------------- 1 | """ 2 | Note: set_up_world_sun_light() is avalible via the bpybb Python package. 3 | https://github.com/CGArtPython/bpy_building_blocks 4 | """ 5 | 6 | import bpy 7 | 8 | import math 9 | import random 10 | 11 | 12 | def set_up_world_sun_light(sun_config=None, strength=1.0): 13 | world_node_tree = bpy.context.scene.world.node_tree 14 | world_node_tree.nodes.clear() 15 | 16 | node_location_x_step = 300 17 | node_location_x = 0 18 | 19 | node_sky = world_node_tree.nodes.new(type="ShaderNodeTexSky") 20 | node_location_x += node_location_x_step 21 | 22 | world_background_node = world_node_tree.nodes.new(type="ShaderNodeBackground") 23 | world_background_node.inputs["Strength"].default_value = strength 24 | world_background_node.location.x = node_location_x 25 | node_location_x += node_location_x_step 26 | 27 | world_output_node = world_node_tree.nodes.new(type="ShaderNodeOutputWorld") 28 | world_output_node.location.x = node_location_x 29 | 30 | if sun_config: 31 | print("Updating ShaderNodeTexSky params:") 32 | for attr, value in sun_config.items(): 33 | if hasattr(node_sky, attr): 34 | print("\t %s set to %s", attr, str(value)) 35 | setattr(node_sky, attr, value) 36 | else: 37 | print("\t warning: %s is not an attribute of ShaderNodeTexSky node", attr) 38 | 39 | world_node_tree.links.new(node_sky.outputs["Color"], world_background_node.inputs["Color"]) 40 | world_node_tree.links.new(world_background_node.outputs["Background"], world_output_node.inputs["Surface"]) 41 | 42 | 43 | def main(): 44 | bpy.ops.mesh.primitive_cube_add(location=(0, 0, 1)) 45 | 46 | bpy.ops.mesh.primitive_plane_add(size=100) 47 | plane_obj = bpy.context.active_object 48 | material = bpy.data.materials.new(name="my_material") 49 | plane_obj.data.materials.append(material) 50 | material.diffuse_color = (0.1, 0.1, 0.1, 1.0) 51 | 52 | sun_config = {"sun_rotation": math.radians(random.randint(0, 360))} 53 | 54 | set_up_world_sun_light(sun_config=sun_config, strength=0.2) 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /bpybb/set_up_world_sun_light/set_up_world_sun_light_start.py: -------------------------------------------------------------------------------- 1 | """ 2 | Note: set_up_world_sun_light() is avalible via the bpybb Python package. 3 | https://github.com/CGArtPython/bpy_building_blocks 4 | """ 5 | 6 | import bpy 7 | 8 | 9 | def set_up_world_sun_light(): 10 | pass 11 | 12 | 13 | def main(): 14 | bpy.ops.mesh.primitive_cube_add(location=(0, 0, 1)) 15 | 16 | bpy.ops.mesh.primitive_plane_add(size=100) 17 | plane_obj = bpy.context.active_object 18 | material = bpy.data.materials.new(name="my_material") 19 | plane_obj.data.materials.append(material) 20 | material.diffuse_color = (0.1, 0.1, 0.1, 1.0) 21 | 22 | set_up_world_sun_light() 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /brainstorming_reverse_key_lighting/script_done.py: -------------------------------------------------------------------------------- 1 | # extend Python's functionality to work with file paths 2 | import pathlib 3 | 4 | # extend Python's functionality to work with JSON files 5 | import json 6 | 7 | # extend Python's functionality to work with time and dates 8 | import datetime 9 | 10 | # extend Python's math functionality 11 | import math 12 | 13 | # give Python access to Blender's functionality 14 | import bpy 15 | 16 | 17 | __rig_obj_tag__ = "brainstorm" 18 | 19 | ################################################################ 20 | # region helper functions BEGIN 21 | ################################################################ 22 | 23 | 24 | def get_output_folder_path(): 25 | return pathlib.Path.home() / "tmp" 26 | 27 | 28 | def get_metadata_folder_path(): 29 | output_path = get_output_folder_path() 30 | metadata_folder_path = output_path / "metadata" 31 | if not metadata_folder_path.exists(): 32 | metadata_folder_path.mkdir() 33 | return metadata_folder_path 34 | 35 | 36 | def parent(child_obj, parent_obj, keep_transform=False): 37 | """Parent the child object to the parent object""" 38 | child_obj.parent = parent_obj 39 | if keep_transform: 40 | child_obj.matrix_parent_inverse = parent_obj.matrix_world.inverted() 41 | 42 | 43 | def create_empty(): 44 | bpy.ops.object.empty_add() 45 | empty_obj = bpy.context.active_object 46 | 47 | empty_obj.name = f"empty.{__rig_obj_tag__}" 48 | 49 | starting_empty_loc = (0.0, 0.0, 0.1) 50 | empty_obj.location = starting_empty_loc 51 | return empty_obj 52 | 53 | 54 | def create_camera(empty_obj): 55 | bpy.ops.object.camera_add() 56 | camera_obj = bpy.context.active_object 57 | 58 | camera_obj.name = f"camera.{__rig_obj_tag__}" 59 | 60 | starting_cam_loc = (1.2, -1.4, 0.9) 61 | camera_obj.location = starting_cam_loc 62 | parent(camera_obj, empty_obj, keep_transform=True) 63 | 64 | bpy.ops.object.constraint_add(type="TRACK_TO") 65 | bpy.context.object.constraints["Track To"].target = empty_obj 66 | 67 | return camera_obj 68 | 69 | 70 | def create_area_light(empty_obj): 71 | bpy.ops.object.light_add(type="AREA") 72 | area_light_obj = bpy.context.active_object 73 | 74 | area_light_obj.name = f"area_light.{__rig_obj_tag__}" 75 | 76 | starting_light_loc = (-10.5, 9.0, 3) 77 | area_light_obj.location = starting_light_loc 78 | starting_light_rot = (0.2, -1.15, -1.0) 79 | area_light_obj.rotation_euler = starting_light_rot 80 | 81 | parent(area_light_obj, empty_obj, keep_transform=True) 82 | 83 | return area_light_obj 84 | 85 | 86 | def create_rig(): 87 | empty_obj = create_empty() 88 | 89 | camera_obj = create_camera(empty_obj) 90 | 91 | area_light_obj = create_area_light(empty_obj) 92 | 93 | return empty_obj, camera_obj, area_light_obj 94 | 95 | 96 | def remove_rig(): 97 | """Remove any rig objects that were left from the previous run""" 98 | objects_to_remove = [obj for obj in bpy.data.objects if __rig_obj_tag__ in obj.name] 99 | 100 | for obj in objects_to_remove: 101 | bpy.data.objects.remove(obj) 102 | 103 | 104 | def scene_setup(full_resolution=False): 105 | remove_rig() 106 | 107 | empty_obj, camera_obj, area_light_obj = create_rig() 108 | 109 | bpy.context.scene.camera = camera_obj 110 | 111 | if full_resolution: 112 | bpy.context.scene.cycles.samples = 300 113 | bpy.context.scene.render.resolution_percentage = 100 114 | else: 115 | bpy.context.scene.cycles.samples = 50 116 | bpy.context.scene.render.resolution_percentage = 50 117 | 118 | return empty_obj, camera_obj, area_light_obj 119 | 120 | 121 | ################################################################ 122 | # endregion helper functions END 123 | ################################################################ 124 | 125 | 126 | def get_scene_configurations(): 127 | return [ 128 | { 129 | "light_power": 1000, 130 | "light_location": (-10.5, 9.0, 3), 131 | "light_rotation": (math.radians(10), math.radians(-65), math.radians(-60)), 132 | "light_scale": (1.0, 1.0, 1.0), 133 | "camera_focal_length": 40, 134 | }, 135 | { 136 | "light_power": 2500, 137 | "light_location": (-10.5, 9.0, 3), 138 | "light_rotation": (math.radians(10), math.radians(-65), math.radians(-20)), 139 | "light_scale": (1.0, 2.0, 1.0), 140 | "camera_focal_length": 50, 141 | }, 142 | { 143 | "light_power": 1500, 144 | "light_location": (-10.5, 9.0, 3), 145 | "light_rotation": (math.radians(10), math.radians(-65), math.radians(-20)), 146 | "light_scale": (1.4, 1.0, 1.0), 147 | "camera_focal_length": 60, 148 | }, 149 | ] 150 | 151 | 152 | def apply_scene_configuration(scene_config, empty_obj, camera_obj, area_light_obj): 153 | empty_obj.rotation_euler.z = math.radians(scene_config["empty_z_rotation"]) 154 | 155 | area_light_obj.location = scene_config["light_location"] 156 | area_light_obj.rotation_euler = scene_config["light_rotation"] 157 | area_light_obj.scale = scene_config["light_scale"] 158 | area_light_obj.data.energy = scene_config["light_power"] 159 | 160 | camera_obj.location.z = scene_config["camera_z_loc"] 161 | camera_obj.data.lens = scene_config["camera_focal_length"] 162 | 163 | 164 | def render_scene(): 165 | 166 | output_folder_path = get_output_folder_path() 167 | time_stamp = datetime.datetime.now().strftime("%H-%M-%S") 168 | image_name = f"{__rig_obj_tag__}_{time_stamp}" 169 | bpy.context.scene.render.filepath = str(output_folder_path / f"{image_name}.png") 170 | 171 | bpy.ops.render.render(write_still=True) 172 | 173 | return image_name 174 | 175 | 176 | def save_scene_configuration(image_name, scene_config): 177 | """Save the scene configuration into a json file and place it into the metadata folder""" 178 | metadata_folder_path = get_metadata_folder_path() 179 | metadata_file_name = str(metadata_folder_path / f"{image_name}.json") 180 | with open(metadata_file_name, "w") as metadata_file_obj: 181 | text = json.dumps(scene_config, indent=4) 182 | metadata_file_obj.write(text) 183 | 184 | 185 | def extract_scene_configuration(image_name): 186 | """Based on the image name find the metadata for that image""" 187 | metadata_folder_path = get_metadata_folder_path() 188 | metadata_file_name = metadata_folder_path / f"{image_name}.json" 189 | 190 | if metadata_file_name.exists(): 191 | with open(str(metadata_file_name), "r") as metadata_file_obj: 192 | text = metadata_file_obj.read() 193 | data = json.loads(text) 194 | return data 195 | else: 196 | print(f"ERROR: {metadata_file_name} scene configuration does not exist") 197 | 198 | return None 199 | 200 | 201 | def load_scene_configuration(image_name): 202 | """Based on the image name find the metadata and apply it""" 203 | empty_obj, camera_obj, area_light_obj = scene_setup(full_resolution=True) 204 | 205 | scene_configuration = extract_scene_configuration(image_name) 206 | 207 | if scene_configuration: 208 | apply_scene_configuration(scene_configuration, empty_obj, camera_obj, area_light_obj) 209 | 210 | 211 | def render_scene_configurations(): 212 | 213 | empty_obj, camera_obj, area_light_obj = scene_setup() 214 | 215 | scene_configurations = get_scene_configurations() 216 | 217 | empty_z_rotation_step = 30 218 | current_empty_z_rotation = 0 219 | 220 | camera_z_loc_start = 0.9 221 | camera_z_loc_step = 0.1 222 | camera_z_loc_step_count = 3 223 | 224 | while current_empty_z_rotation < 360: 225 | 226 | current_camera_z_loc = camera_z_loc_start 227 | 228 | for _ in range(camera_z_loc_step_count): 229 | for scene_config in scene_configurations: 230 | scene_config["empty_z_rotation"] = current_empty_z_rotation 231 | scene_config["camera_z_loc"] = current_camera_z_loc 232 | 233 | apply_scene_configuration(scene_config, empty_obj, camera_obj, area_light_obj) 234 | 235 | image_name = render_scene() 236 | 237 | save_scene_configuration(image_name, scene_config) 238 | 239 | current_camera_z_loc -= camera_z_loc_step 240 | 241 | current_empty_z_rotation += empty_z_rotation_step 242 | 243 | 244 | def main(): 245 | """ 246 | Brainstorming Reverse Key Lighting scene setup. 247 | 248 | Inspired by Gleb Alexandrov's tutorial 249 | One Simple Technique to Improve Your Lighting in Blender | Reverse Key Lighting 250 | https://www.youtube.com/watch?v=jrCtpmdAhF0 251 | """ 252 | # If you like one of the created images you can set load_scene_config to True and 253 | # set image_name to the image name 254 | load_scene_config = False 255 | 256 | if load_scene_config: 257 | image_name = "brainstorm_15-34-16" 258 | load_scene_configuration(image_name) 259 | else: 260 | render_scene_configurations() 261 | 262 | 263 | if __name__ == "__main__": 264 | main() 265 | -------------------------------------------------------------------------------- /brainstorming_reverse_key_lighting/script_start.py: -------------------------------------------------------------------------------- 1 | # extend Python's functionality to work with file paths 2 | import pathlib 3 | 4 | # extend Python's functionality to work with JSON files 5 | import json 6 | 7 | # extend Python's functionality to work with time and dates 8 | import datetime 9 | 10 | # extend Python's math functionality 11 | import math 12 | 13 | # give Python access to Blender's functionality 14 | import bpy 15 | 16 | 17 | __rig_obj_tag__ = "brainstorm" 18 | 19 | ################################################################ 20 | # region helper functions BEGIN 21 | ################################################################ 22 | 23 | 24 | def get_output_folder_path(): 25 | return pathlib.Path.home() / "tmp" 26 | 27 | 28 | def get_metadata_folder_path(): 29 | output_path = get_output_folder_path() 30 | metadata_folder_path = output_path / "metadata" 31 | if not metadata_folder_path.exists(): 32 | metadata_folder_path.mkdir() 33 | return metadata_folder_path 34 | 35 | 36 | def parent(child_obj, parent_obj, keep_transform=False): 37 | """Parent the child object to the parent object""" 38 | child_obj.parent = parent_obj 39 | if keep_transform: 40 | child_obj.matrix_parent_inverse = parent_obj.matrix_world.inverted() 41 | 42 | 43 | def create_empty(): 44 | bpy.ops.object.empty_add() 45 | empty_obj = bpy.context.active_object 46 | 47 | empty_obj.name = f"empty.{__rig_obj_tag__}" 48 | 49 | starting_empty_loc = (0.0, 0.0, 0.1) 50 | empty_obj.location = starting_empty_loc 51 | return empty_obj 52 | 53 | 54 | def create_camera(empty_obj): 55 | bpy.ops.object.camera_add() 56 | camera_obj = bpy.context.active_object 57 | 58 | camera_obj.name = f"camera.{__rig_obj_tag__}" 59 | 60 | starting_cam_loc = (1.2, -1.4, 0.9) 61 | camera_obj.location = starting_cam_loc 62 | parent(camera_obj, empty_obj, keep_transform=True) 63 | 64 | bpy.ops.object.constraint_add(type="TRACK_TO") 65 | bpy.context.object.constraints["Track To"].target = empty_obj 66 | 67 | return camera_obj 68 | 69 | 70 | def create_area_light(empty_obj): 71 | bpy.ops.object.light_add(type="AREA") 72 | area_light_obj = bpy.context.active_object 73 | 74 | area_light_obj.name = f"area_light.{__rig_obj_tag__}" 75 | 76 | starting_light_loc = (-10.5, 9.0, 3) 77 | area_light_obj.location = starting_light_loc 78 | starting_light_rot = (0.2, -1.15, -1.0) 79 | area_light_obj.rotation_euler = starting_light_rot 80 | 81 | parent(area_light_obj, empty_obj, keep_transform=True) 82 | 83 | return area_light_obj 84 | 85 | 86 | def create_rig(): 87 | empty_obj = create_empty() 88 | 89 | camera_obj = create_camera(empty_obj) 90 | 91 | area_light_obj = create_area_light(empty_obj) 92 | 93 | return empty_obj, camera_obj, area_light_obj 94 | 95 | 96 | def remove_rig(): 97 | """Remove any rig objects that were left from the previous run""" 98 | objects_to_remove = [obj for obj in bpy.data.objects if __rig_obj_tag__ in obj.name] 99 | 100 | for obj in objects_to_remove: 101 | bpy.data.objects.remove(obj) 102 | 103 | 104 | def scene_setup(full_resolution=False): 105 | remove_rig() 106 | 107 | empty_obj, camera_obj, area_light_obj = create_rig() 108 | 109 | bpy.context.scene.camera = camera_obj 110 | 111 | if full_resolution: 112 | bpy.context.scene.cycles.samples = 300 113 | bpy.context.scene.render.resolution_percentage = 100 114 | else: 115 | bpy.context.scene.cycles.samples = 50 116 | bpy.context.scene.render.resolution_percentage = 50 117 | 118 | return empty_obj, camera_obj, area_light_obj 119 | 120 | 121 | ################################################################ 122 | # endregion helper functions END 123 | ################################################################ 124 | 125 | 126 | def render_scene_configurations(): 127 | 128 | empty_obj, camera_obj, area_light_obj = scene_setup() 129 | 130 | 131 | def main(): 132 | """ 133 | Brainstorming Reverse Key Lighting scene setup. 134 | 135 | Inspired by Gleb Alexandrov's tutorial 136 | One Simple Technique to Improve Your Lighting in Blender | Reverse Key Lighting 137 | https://www.youtube.com/watch?v=jrCtpmdAhF0 138 | """ 139 | render_scene_configurations() 140 | 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /color_slices/color_slices_part1_done.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/aeDbYuJyXr8 3 | """ 4 | 5 | import random 6 | import time 7 | import math 8 | 9 | import bpy 10 | import mathutils 11 | 12 | ################################################################ 13 | # helper functions BEGIN 14 | ################################################################ 15 | 16 | 17 | def purge_orphans(): 18 | """ 19 | Remove all orphan data blocks 20 | 21 | see this from more info: 22 | https://youtu.be/3rNqVPtbhzc?t=149 23 | """ 24 | if bpy.app.version >= (3, 0, 0): 25 | # run this only for Blender versions 3.0 and higher 26 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 27 | else: 28 | # run this only for Blender versions lower than 3.0 29 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 30 | result = bpy.ops.outliner.orphans_purge() 31 | if result.pop() != "CANCELLED": 32 | purge_orphans() 33 | 34 | 35 | def clean_scene(): 36 | """ 37 | Removing all of the objects, collection, materials, particles, 38 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 39 | 40 | Checkout this video explanation with example 41 | 42 | "How to clean the scene with Python in Blender (with examples)" 43 | https://youtu.be/3rNqVPtbhzc 44 | """ 45 | # make sure the active object is not in Edit Mode 46 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 47 | bpy.ops.object.editmode_toggle() 48 | 49 | # make sure non of the objects are hidden from the viewport, selection, or disabled 50 | for obj in bpy.data.objects: 51 | obj.hide_set(False) 52 | obj.hide_select = False 53 | obj.hide_viewport = False 54 | 55 | # select all the object and delete them (just like pressing A + X + D in the viewport) 56 | bpy.ops.object.select_all(action="SELECT") 57 | bpy.ops.object.delete() 58 | 59 | # find all the collections and remove them 60 | collection_names = [col.name for col in bpy.data.collections] 61 | for name in collection_names: 62 | bpy.data.collections.remove(bpy.data.collections[name]) 63 | 64 | # in the case when you modify the world shader 65 | # delete and recreate the world object 66 | world_names = [world.name for world in bpy.data.worlds] 67 | for name in world_names: 68 | bpy.data.worlds.remove(bpy.data.worlds[name]) 69 | # create a new world data block 70 | bpy.ops.world.new() 71 | bpy.context.scene.world = bpy.data.worlds["World"] 72 | 73 | purge_orphans() 74 | 75 | 76 | def get_random_pallet_color(context): 77 | return random.choice(context["colors"]) 78 | 79 | 80 | def active_object(): 81 | """ 82 | returns the active object 83 | """ 84 | return bpy.context.active_object 85 | 86 | 87 | def time_seed(): 88 | """ 89 | Sets the random seed based on the time 90 | and copies the seed into the clipboard 91 | """ 92 | seed = time.time() 93 | print(f"seed: {seed}") 94 | random.seed(seed) 95 | 96 | # add the seed value to your clipboard 97 | bpy.context.window_manager.clipboard = str(seed) 98 | 99 | return seed 100 | 101 | 102 | def add_ctrl_empty(name=None): 103 | 104 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 105 | empty_ctrl = active_object() 106 | 107 | if name: 108 | empty_ctrl.name = name 109 | else: 110 | empty_ctrl.name = "empty.cntrl" 111 | 112 | return empty_ctrl 113 | 114 | 115 | def apply_material(material): 116 | obj = active_object() 117 | obj.data.materials.append(material) 118 | 119 | 120 | def make_active(obj): 121 | bpy.ops.object.select_all(action="DESELECT") 122 | obj.select_set(True) 123 | bpy.context.view_layer.objects.active = obj 124 | 125 | 126 | def track_empty(obj): 127 | """ 128 | create an empty and add a 'Track To' constraint 129 | """ 130 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 131 | 132 | make_active(obj) 133 | bpy.ops.object.constraint_add(type="TRACK_TO") 134 | bpy.context.object.constraints["Track To"].target = empty 135 | 136 | return empty 137 | 138 | 139 | def setup_camera(loc, rot): 140 | """ 141 | create and setup the camera 142 | """ 143 | bpy.ops.object.camera_add(location=loc, rotation=rot) 144 | camera = active_object() 145 | 146 | # set the camera as the "active camera" in the scene 147 | bpy.context.scene.camera = camera 148 | 149 | # set the Focal Length of the camera 150 | camera.data.lens = 70 151 | 152 | camera.data.passepartout_alpha = 0.9 153 | 154 | empty = track_empty(camera) 155 | 156 | return empty 157 | 158 | 159 | def set_1k_square_render_res(): 160 | """ 161 | Set the resolution of the rendered image to 1080 by 1080 162 | """ 163 | bpy.context.scene.render.resolution_x = 1080 164 | bpy.context.scene.render.resolution_y = 1080 165 | 166 | 167 | def set_scene_props(fps, loop_seconds): 168 | """ 169 | Set scene properties 170 | """ 171 | frame_count = fps * loop_seconds 172 | 173 | scene = bpy.context.scene 174 | scene.frame_end = frame_count 175 | 176 | # set the world background to black 177 | world = bpy.data.worlds["World"] 178 | if "Background" in world.node_tree.nodes: 179 | world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) 180 | 181 | scene.render.fps = fps 182 | 183 | scene.frame_current = 1 184 | scene.frame_start = 1 185 | 186 | scene.eevee.use_bloom = True 187 | scene.eevee.bloom_intensity = 0.005 188 | 189 | # set Ambient Occlusion properties 190 | scene.eevee.use_gtao = True 191 | scene.eevee.gtao_distance = 4 192 | scene.eevee.gtao_factor = 5 193 | 194 | scene.eevee.taa_render_samples = 64 195 | 196 | scene.view_settings.look = "Very High Contrast" 197 | 198 | set_1k_square_render_res() 199 | 200 | 201 | def make_ramp_from_colors(colors, color_ramp_node): 202 | """ 203 | Creates new sliders on a Color Ramp Node and 204 | applies the list of colors on each slider 205 | """ 206 | color_count = len(colors) 207 | 208 | step = 1 / color_count 209 | cur_pos = step 210 | # -2 is for the two sliders that are present on the ramp 211 | for _ in range(color_count - 2): 212 | color_ramp_node.elements.new(cur_pos) 213 | cur_pos += step 214 | 215 | for i, color in enumerate(colors): 216 | color_ramp_node.elements[i].color = color 217 | 218 | 219 | def get_color_palette(): 220 | # https://www.colourlovers.com/palette/2943292 221 | # palette = ['#D7CEA3FF', '#907826FF', '#A46719FF', '#CE3F0EFF', '#1A0C47FF'] 222 | 223 | palette = [ 224 | [0.83984375, 0.8046875, 0.63671875, 1.0], 225 | [0.5625, 0.46875, 0.1484375, 1.0], 226 | [0.640625, 0.40234375, 0.09765625, 1.0], 227 | [0.8046875, 0.24609375, 0.0546875, 1.0], 228 | [0.1015625, 0.046875, 0.27734375, 1.0], 229 | ] 230 | 231 | return palette 232 | 233 | 234 | def apply_location(): 235 | bpy.ops.object.transform_apply(location=True) 236 | 237 | 238 | def setup_scene(): 239 | fps = 30 240 | loop_seconds = 6 241 | frame_count = fps * loop_seconds 242 | 243 | project_name = "color_slices" 244 | bpy.context.scene.render.image_settings.file_format = "PNG" 245 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/" 246 | 247 | seed = 0 248 | if seed: 249 | random.seed(seed) 250 | else: 251 | time_seed() 252 | 253 | # Utility Building Blocks 254 | clean_scene() 255 | set_scene_props(fps, loop_seconds) 256 | 257 | loc = (0, 0, 5) 258 | rot = (0, 0, 0) 259 | setup_camera(loc, rot) 260 | 261 | context = { 262 | "frame_count": frame_count, 263 | } 264 | 265 | context["colors"] = get_color_palette() 266 | 267 | return context 268 | 269 | 270 | ################################################################ 271 | # helper functions END 272 | ################################################################ 273 | 274 | 275 | def main(): 276 | """ 277 | Python code for this art project 278 | https://www.artstation.com/artwork/48wX6L 279 | 280 | Tutorial Video about this script: 281 | https://www.youtube.com/watch?v=a6sfV07bRzM&list=PLB8-FQgROBmlzQ7Xkq4YU7u08Zh3iuyPD&index=2 282 | """ 283 | context = setup_scene() 284 | 285 | 286 | if __name__ == "__main__": 287 | main() 288 | -------------------------------------------------------------------------------- /color_slices/color_slices_part2_done.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/R4CEZgw7nJU 3 | """ 4 | import random 5 | import time 6 | import math 7 | 8 | import bpy 9 | import mathutils 10 | 11 | ################################################################ 12 | # helper functions BEGIN 13 | ################################################################ 14 | 15 | 16 | def purge_orphans(): 17 | """ 18 | Remove all orphan data blocks 19 | 20 | see this from more info: 21 | https://youtu.be/3rNqVPtbhzc?t=149 22 | """ 23 | if bpy.app.version >= (3, 0, 0): 24 | # run this only for Blender versions 3.0 and higher 25 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 26 | else: 27 | # run this only for Blender versions lower than 3.0 28 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 29 | result = bpy.ops.outliner.orphans_purge() 30 | if result.pop() != "CANCELLED": 31 | purge_orphans() 32 | 33 | 34 | def clean_scene(): 35 | """ 36 | Removing all of the objects, collection, materials, particles, 37 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 38 | 39 | Checkout this video explanation with example 40 | 41 | "How to clean the scene with Python in Blender (with examples)" 42 | https://youtu.be/3rNqVPtbhzc 43 | """ 44 | # make sure the active object is not in Edit Mode 45 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 46 | bpy.ops.object.editmode_toggle() 47 | 48 | # make sure non of the objects are hidden from the viewport, selection, or disabled 49 | for obj in bpy.data.objects: 50 | obj.hide_set(False) 51 | obj.hide_select = False 52 | obj.hide_viewport = False 53 | 54 | # select all the object and delete them (just like pressing A + X + D in the viewport) 55 | bpy.ops.object.select_all(action="SELECT") 56 | bpy.ops.object.delete() 57 | 58 | # find all the collections and remove them 59 | collection_names = [col.name for col in bpy.data.collections] 60 | for name in collection_names: 61 | bpy.data.collections.remove(bpy.data.collections[name]) 62 | 63 | # in the case when you modify the world shader 64 | # delete and recreate the world object 65 | world_names = [world.name for world in bpy.data.worlds] 66 | for name in world_names: 67 | bpy.data.worlds.remove(bpy.data.worlds[name]) 68 | # create a new world data block 69 | bpy.ops.world.new() 70 | bpy.context.scene.world = bpy.data.worlds["World"] 71 | 72 | purge_orphans() 73 | 74 | 75 | def get_random_pallet_color(context): 76 | return random.choice(context["colors"]) 77 | 78 | 79 | def active_object(): 80 | """ 81 | returns the active object 82 | """ 83 | return bpy.context.active_object 84 | 85 | 86 | def time_seed(): 87 | """ 88 | Sets the random seed based on the time 89 | and copies the seed into the clipboard 90 | """ 91 | seed = time.time() 92 | print(f"seed: {seed}") 93 | random.seed(seed) 94 | 95 | # add the seed value to your clipboard 96 | bpy.context.window_manager.clipboard = str(seed) 97 | 98 | return seed 99 | 100 | 101 | def add_ctrl_empty(name=None): 102 | 103 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 104 | empty_ctrl = active_object() 105 | 106 | if name: 107 | empty_ctrl.name = name 108 | else: 109 | empty_ctrl.name = "empty.cntrl" 110 | 111 | return empty_ctrl 112 | 113 | 114 | def apply_material(material): 115 | obj = active_object() 116 | obj.data.materials.append(material) 117 | 118 | 119 | def make_active(obj): 120 | bpy.ops.object.select_all(action="DESELECT") 121 | obj.select_set(True) 122 | bpy.context.view_layer.objects.active = obj 123 | 124 | 125 | def track_empty(obj): 126 | """ 127 | create an empty and add a 'Track To' constraint 128 | """ 129 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 130 | 131 | make_active(obj) 132 | bpy.ops.object.constraint_add(type="TRACK_TO") 133 | bpy.context.object.constraints["Track To"].target = empty 134 | 135 | return empty 136 | 137 | 138 | def setup_camera(loc, rot): 139 | """ 140 | create and setup the camera 141 | """ 142 | bpy.ops.object.camera_add(location=loc, rotation=rot) 143 | camera = active_object() 144 | 145 | # set the camera as the "active camera" in the scene 146 | bpy.context.scene.camera = camera 147 | 148 | # set the Focal Length of the camera 149 | camera.data.lens = 70 150 | 151 | camera.data.passepartout_alpha = 0.9 152 | 153 | empty = track_empty(camera) 154 | 155 | return empty 156 | 157 | 158 | def set_1k_square_render_res(): 159 | """ 160 | Set the resolution of the rendered image to 1080 by 1080 161 | """ 162 | bpy.context.scene.render.resolution_x = 1080 163 | bpy.context.scene.render.resolution_y = 1080 164 | 165 | 166 | def set_scene_props(fps, loop_seconds): 167 | """ 168 | Set scene properties 169 | """ 170 | frame_count = fps * loop_seconds 171 | 172 | scene = bpy.context.scene 173 | scene.frame_end = frame_count 174 | 175 | # set the world background to black 176 | world = bpy.data.worlds["World"] 177 | if "Background" in world.node_tree.nodes: 178 | world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) 179 | 180 | scene.render.fps = fps 181 | 182 | scene.frame_current = 1 183 | scene.frame_start = 1 184 | 185 | scene.eevee.use_bloom = True 186 | scene.eevee.bloom_intensity = 0.005 187 | 188 | # set Ambient Occlusion properties 189 | scene.eevee.use_gtao = True 190 | scene.eevee.gtao_distance = 4 191 | scene.eevee.gtao_factor = 5 192 | 193 | scene.eevee.taa_render_samples = 64 194 | 195 | scene.view_settings.look = "Very High Contrast" 196 | 197 | set_1k_square_render_res() 198 | 199 | 200 | def make_ramp_from_colors(colors, color_ramp_node): 201 | """ 202 | Creates new sliders on a Color Ramp Node and 203 | applies the list of colors on each slider 204 | """ 205 | color_count = len(colors) 206 | 207 | step = 1 / color_count 208 | cur_pos = step 209 | # -2 is for the two sliders that are present on the ramp 210 | for _ in range(color_count - 2): 211 | color_ramp_node.elements.new(cur_pos) 212 | cur_pos += step 213 | 214 | for i, color in enumerate(colors): 215 | color_ramp_node.elements[i].color = color 216 | 217 | 218 | def get_color_palette(): 219 | # https://www.colourlovers.com/palette/2943292 220 | # palette = ['#D7CEA3FF', '#907826FF', '#A46719FF', '#CE3F0EFF', '#1A0C47FF'] 221 | 222 | palette = [ 223 | [0.83984375, 0.8046875, 0.63671875, 1.0], 224 | [0.5625, 0.46875, 0.1484375, 1.0], 225 | [0.640625, 0.40234375, 0.09765625, 1.0], 226 | [0.8046875, 0.24609375, 0.0546875, 1.0], 227 | [0.1015625, 0.046875, 0.27734375, 1.0], 228 | ] 229 | 230 | return palette 231 | 232 | 233 | def apply_location(): 234 | bpy.ops.object.transform_apply(location=True) 235 | 236 | 237 | def setup_scene(): 238 | fps = 30 239 | loop_seconds = 6 240 | frame_count = fps * loop_seconds 241 | 242 | project_name = "color_slices" 243 | bpy.context.scene.render.image_settings.file_format = "PNG" 244 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/" 245 | 246 | seed = 0 247 | if seed: 248 | random.seed(seed) 249 | else: 250 | time_seed() 251 | 252 | # Utility Building Blocks 253 | clean_scene() 254 | set_scene_props(fps, loop_seconds) 255 | 256 | loc = (0, 0, 5) 257 | rot = (0, 0, 0) 258 | setup_camera(loc, rot) 259 | 260 | context = { 261 | "frame_count": frame_count, 262 | } 263 | 264 | context["colors"] = get_color_palette() 265 | 266 | return context 267 | 268 | 269 | ################################################################ 270 | # helper functions END 271 | ################################################################ 272 | 273 | 274 | def gen_perlin_curve(): 275 | 276 | bpy.ops.mesh.primitive_circle_add(vertices=512, radius=1) 277 | circle = active_object() 278 | 279 | deform_coords = [] 280 | 281 | for vert in circle.data.vertices: 282 | new_location = vert.co 283 | noise_value = mathutils.noise.noise(new_location) 284 | noise_value = noise_value / 2 285 | 286 | deform_vector = vert.co * noise_value 287 | 288 | deform_coord = vert.co + deform_vector 289 | deform_coords.append(deform_coord) 290 | 291 | bpy.ops.object.convert(target="CURVE") 292 | curve_obj = active_object() 293 | 294 | 295 | def main(): 296 | """ 297 | Python code for this art project 298 | https://www.artstation.com/artwork/48wX6L 299 | 300 | Tutorial Video about this script: 301 | https://www.youtube.com/watch?v=R4CEZgw7nJU&list=PLB8-FQgROBmlzQ7Xkq4YU7u08Zh3iuyPD&index=3 302 | """ 303 | context = setup_scene() 304 | 305 | gen_perlin_curve() 306 | 307 | 308 | if __name__ == "__main__": 309 | main() 310 | -------------------------------------------------------------------------------- /color_slices/color_slices_part2_start.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/R4CEZgw7nJU 3 | """ 4 | import random 5 | import time 6 | import math 7 | 8 | import bpy 9 | import mathutils 10 | 11 | ################################################################ 12 | # helper functions BEGIN 13 | ################################################################ 14 | 15 | 16 | def purge_orphans(): 17 | """ 18 | Remove all orphan data blocks 19 | 20 | see this from more info: 21 | https://youtu.be/3rNqVPtbhzc?t=149 22 | """ 23 | if bpy.app.version >= (3, 0, 0): 24 | # run this only for Blender versions 3.0 and higher 25 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 26 | else: 27 | # run this only for Blender versions lower than 3.0 28 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 29 | result = bpy.ops.outliner.orphans_purge() 30 | if result.pop() != "CANCELLED": 31 | purge_orphans() 32 | 33 | 34 | def clean_scene(): 35 | """ 36 | Removing all of the objects, collection, materials, particles, 37 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 38 | 39 | Checkout this video explanation with example 40 | 41 | "How to clean the scene with Python in Blender (with examples)" 42 | https://youtu.be/3rNqVPtbhzc 43 | """ 44 | # make sure the active object is not in Edit Mode 45 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 46 | bpy.ops.object.editmode_toggle() 47 | 48 | # make sure non of the objects are hidden from the viewport, selection, or disabled 49 | for obj in bpy.data.objects: 50 | obj.hide_set(False) 51 | obj.hide_select = False 52 | obj.hide_viewport = False 53 | 54 | # select all the object and delete them (just like pressing A + X + D in the viewport) 55 | bpy.ops.object.select_all(action="SELECT") 56 | bpy.ops.object.delete() 57 | 58 | # find all the collections and remove them 59 | collection_names = [col.name for col in bpy.data.collections] 60 | for name in collection_names: 61 | bpy.data.collections.remove(bpy.data.collections[name]) 62 | 63 | # in the case when you modify the world shader 64 | # delete and recreate the world object 65 | world_names = [world.name for world in bpy.data.worlds] 66 | for name in world_names: 67 | bpy.data.worlds.remove(bpy.data.worlds[name]) 68 | # create a new world data block 69 | bpy.ops.world.new() 70 | bpy.context.scene.world = bpy.data.worlds["World"] 71 | 72 | purge_orphans() 73 | 74 | 75 | def get_random_pallet_color(context): 76 | return random.choice(context["colors"]) 77 | 78 | 79 | def active_object(): 80 | """ 81 | returns the active object 82 | """ 83 | return bpy.context.active_object 84 | 85 | 86 | def time_seed(): 87 | """ 88 | Sets the random seed based on the time 89 | and copies the seed into the clipboard 90 | """ 91 | seed = time.time() 92 | print(f"seed: {seed}") 93 | random.seed(seed) 94 | 95 | # add the seed value to your clipboard 96 | bpy.context.window_manager.clipboard = str(seed) 97 | 98 | return seed 99 | 100 | 101 | def add_ctrl_empty(name=None): 102 | 103 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 104 | empty_ctrl = active_object() 105 | 106 | if name: 107 | empty_ctrl.name = name 108 | else: 109 | empty_ctrl.name = "empty.cntrl" 110 | 111 | return empty_ctrl 112 | 113 | 114 | def apply_material(material): 115 | obj = active_object() 116 | obj.data.materials.append(material) 117 | 118 | 119 | def make_active(obj): 120 | bpy.ops.object.select_all(action="DESELECT") 121 | obj.select_set(True) 122 | bpy.context.view_layer.objects.active = obj 123 | 124 | 125 | def track_empty(obj): 126 | """ 127 | create an empty and add a 'Track To' constraint 128 | """ 129 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 130 | 131 | make_active(obj) 132 | bpy.ops.object.constraint_add(type="TRACK_TO") 133 | bpy.context.object.constraints["Track To"].target = empty 134 | 135 | return empty 136 | 137 | 138 | def setup_camera(loc, rot): 139 | """ 140 | create and setup the camera 141 | """ 142 | bpy.ops.object.camera_add(location=loc, rotation=rot) 143 | camera = active_object() 144 | 145 | # set the camera as the "active camera" in the scene 146 | bpy.context.scene.camera = camera 147 | 148 | # set the Focal Length of the camera 149 | camera.data.lens = 70 150 | 151 | camera.data.passepartout_alpha = 0.9 152 | 153 | empty = track_empty(camera) 154 | 155 | return empty 156 | 157 | 158 | def set_1k_square_render_res(): 159 | """ 160 | Set the resolution of the rendered image to 1080 by 1080 161 | """ 162 | bpy.context.scene.render.resolution_x = 1080 163 | bpy.context.scene.render.resolution_y = 1080 164 | 165 | 166 | def set_scene_props(fps, loop_seconds): 167 | """ 168 | Set scene properties 169 | """ 170 | frame_count = fps * loop_seconds 171 | 172 | scene = bpy.context.scene 173 | scene.frame_end = frame_count 174 | 175 | # set the world background to black 176 | world = bpy.data.worlds["World"] 177 | if "Background" in world.node_tree.nodes: 178 | world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) 179 | 180 | scene.render.fps = fps 181 | 182 | scene.frame_current = 1 183 | scene.frame_start = 1 184 | 185 | scene.eevee.use_bloom = True 186 | scene.eevee.bloom_intensity = 0.005 187 | 188 | # set Ambient Occlusion properties 189 | scene.eevee.use_gtao = True 190 | scene.eevee.gtao_distance = 4 191 | scene.eevee.gtao_factor = 5 192 | 193 | scene.eevee.taa_render_samples = 64 194 | 195 | scene.view_settings.look = "Very High Contrast" 196 | 197 | set_1k_square_render_res() 198 | 199 | 200 | def make_ramp_from_colors(colors, color_ramp_node): 201 | """ 202 | Creates new sliders on a Color Ramp Node and 203 | applies the list of colors on each slider 204 | """ 205 | color_count = len(colors) 206 | 207 | step = 1 / color_count 208 | cur_pos = step 209 | # -2 is for the two sliders that are present on the ramp 210 | for _ in range(color_count - 2): 211 | color_ramp_node.elements.new(cur_pos) 212 | cur_pos += step 213 | 214 | for i, color in enumerate(colors): 215 | color_ramp_node.elements[i].color = color 216 | 217 | 218 | def get_color_palette(): 219 | # https://www.colourlovers.com/palette/2943292 220 | # palette = ['#D7CEA3FF', '#907826FF', '#A46719FF', '#CE3F0EFF', '#1A0C47FF'] 221 | 222 | palette = [ 223 | [0.83984375, 0.8046875, 0.63671875, 1.0], 224 | [0.5625, 0.46875, 0.1484375, 1.0], 225 | [0.640625, 0.40234375, 0.09765625, 1.0], 226 | [0.8046875, 0.24609375, 0.0546875, 1.0], 227 | [0.1015625, 0.046875, 0.27734375, 1.0], 228 | ] 229 | 230 | return palette 231 | 232 | 233 | def apply_location(): 234 | bpy.ops.object.transform_apply(location=True) 235 | 236 | 237 | def setup_scene(): 238 | fps = 30 239 | loop_seconds = 6 240 | frame_count = fps * loop_seconds 241 | 242 | project_name = "color_slices" 243 | bpy.context.scene.render.image_settings.file_format = "PNG" 244 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/" 245 | 246 | seed = 0 247 | if seed: 248 | random.seed(seed) 249 | else: 250 | time_seed() 251 | 252 | # Utility Building Blocks 253 | clean_scene() 254 | set_scene_props(fps, loop_seconds) 255 | 256 | loc = (0, 0, 5) 257 | rot = (0, 0, 0) 258 | setup_camera(loc, rot) 259 | 260 | context = { 261 | "frame_count": frame_count, 262 | } 263 | 264 | context["colors"] = get_color_palette() 265 | 266 | return context 267 | 268 | 269 | ################################################################ 270 | # helper functions END 271 | ################################################################ 272 | 273 | 274 | def main(): 275 | """ 276 | Python code for this art project 277 | https://www.artstation.com/artwork/48wX6L 278 | 279 | Tutorial Video about this script: 280 | https://www.youtube.com/watch?v=R4CEZgw7nJU&list=PLB8-FQgROBmlzQ7Xkq4YU7u08Zh3iuyPD&index=3 281 | """ 282 | context = setup_scene() 283 | 284 | 285 | if __name__ == "__main__": 286 | main() 287 | -------------------------------------------------------------------------------- /color_slices/color_slices_part3_done.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/5cYn5WuyiWI 3 | """ 4 | import random 5 | import time 6 | import math 7 | 8 | import bpy 9 | import mathutils 10 | 11 | ################################################################ 12 | # helper functions BEGIN 13 | ################################################################ 14 | 15 | 16 | def purge_orphans(): 17 | """ 18 | Remove all orphan data blocks 19 | 20 | see this from more info: 21 | https://youtu.be/3rNqVPtbhzc?t=149 22 | """ 23 | if bpy.app.version >= (3, 0, 0): 24 | # run this only for Blender versions 3.0 and higher 25 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 26 | else: 27 | # run this only for Blender versions lower than 3.0 28 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 29 | result = bpy.ops.outliner.orphans_purge() 30 | if result.pop() != "CANCELLED": 31 | purge_orphans() 32 | 33 | 34 | def clean_scene(): 35 | """ 36 | Removing all of the objects, collection, materials, particles, 37 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 38 | 39 | Checkout this video explanation with example 40 | 41 | "How to clean the scene with Python in Blender (with examples)" 42 | https://youtu.be/3rNqVPtbhzc 43 | """ 44 | # make sure the active object is not in Edit Mode 45 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 46 | bpy.ops.object.editmode_toggle() 47 | 48 | # make sure non of the objects are hidden from the viewport, selection, or disabled 49 | for obj in bpy.data.objects: 50 | obj.hide_set(False) 51 | obj.hide_select = False 52 | obj.hide_viewport = False 53 | 54 | # select all the object and delete them (just like pressing A + X + D in the viewport) 55 | bpy.ops.object.select_all(action="SELECT") 56 | bpy.ops.object.delete() 57 | 58 | # find all the collections and remove them 59 | collection_names = [col.name for col in bpy.data.collections] 60 | for name in collection_names: 61 | bpy.data.collections.remove(bpy.data.collections[name]) 62 | 63 | # in the case when you modify the world shader 64 | # delete and recreate the world object 65 | world_names = [world.name for world in bpy.data.worlds] 66 | for name in world_names: 67 | bpy.data.worlds.remove(bpy.data.worlds[name]) 68 | # create a new world data block 69 | bpy.ops.world.new() 70 | bpy.context.scene.world = bpy.data.worlds["World"] 71 | 72 | purge_orphans() 73 | 74 | 75 | def get_random_pallet_color(context): 76 | return random.choice(context["colors"]) 77 | 78 | 79 | def active_object(): 80 | """ 81 | returns the active object 82 | """ 83 | return bpy.context.active_object 84 | 85 | 86 | def time_seed(): 87 | """ 88 | Sets the random seed based on the time 89 | and copies the seed into the clipboard 90 | """ 91 | seed = time.time() 92 | print(f"seed: {seed}") 93 | random.seed(seed) 94 | 95 | # add the seed value to your clipboard 96 | bpy.context.window_manager.clipboard = str(seed) 97 | 98 | return seed 99 | 100 | 101 | def add_ctrl_empty(name=None): 102 | 103 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 104 | empty_ctrl = active_object() 105 | 106 | if name: 107 | empty_ctrl.name = name 108 | else: 109 | empty_ctrl.name = "empty.cntrl" 110 | 111 | return empty_ctrl 112 | 113 | 114 | def apply_material(material): 115 | obj = active_object() 116 | obj.data.materials.append(material) 117 | 118 | 119 | def make_active(obj): 120 | bpy.ops.object.select_all(action="DESELECT") 121 | obj.select_set(True) 122 | bpy.context.view_layer.objects.active = obj 123 | 124 | 125 | def track_empty(obj): 126 | """ 127 | create an empty and add a 'Track To' constraint 128 | """ 129 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 130 | 131 | make_active(obj) 132 | bpy.ops.object.constraint_add(type="TRACK_TO") 133 | bpy.context.object.constraints["Track To"].target = empty 134 | 135 | return empty 136 | 137 | 138 | def setup_camera(loc, rot): 139 | """ 140 | create and setup the camera 141 | """ 142 | bpy.ops.object.camera_add(location=loc, rotation=rot) 143 | camera = active_object() 144 | 145 | # set the camera as the "active camera" in the scene 146 | bpy.context.scene.camera = camera 147 | 148 | # set the Focal Length of the camera 149 | camera.data.lens = 70 150 | 151 | camera.data.passepartout_alpha = 0.9 152 | 153 | empty = track_empty(camera) 154 | 155 | return empty 156 | 157 | 158 | def set_1k_square_render_res(): 159 | """ 160 | Set the resolution of the rendered image to 1080 by 1080 161 | """ 162 | bpy.context.scene.render.resolution_x = 1080 163 | bpy.context.scene.render.resolution_y = 1080 164 | 165 | 166 | def set_scene_props(fps, loop_seconds): 167 | """ 168 | Set scene properties 169 | """ 170 | frame_count = fps * loop_seconds 171 | 172 | scene = bpy.context.scene 173 | scene.frame_end = frame_count 174 | 175 | # set the world background to black 176 | world = bpy.data.worlds["World"] 177 | if "Background" in world.node_tree.nodes: 178 | world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) 179 | 180 | scene.render.fps = fps 181 | 182 | scene.frame_current = 1 183 | scene.frame_start = 1 184 | 185 | scene.eevee.use_bloom = True 186 | scene.eevee.bloom_intensity = 0.005 187 | 188 | # set Ambient Occlusion properties 189 | scene.eevee.use_gtao = True 190 | scene.eevee.gtao_distance = 4 191 | scene.eevee.gtao_factor = 5 192 | 193 | scene.eevee.taa_render_samples = 64 194 | 195 | scene.view_settings.look = "Very High Contrast" 196 | 197 | set_1k_square_render_res() 198 | 199 | 200 | def make_ramp_from_colors(colors, color_ramp_node): 201 | """ 202 | Creates new sliders on a Color Ramp Node and 203 | applies the list of colors on each slider 204 | """ 205 | color_count = len(colors) 206 | 207 | step = 1 / color_count 208 | cur_pos = step 209 | # -2 is for the two sliders that are present on the ramp 210 | for _ in range(color_count - 2): 211 | color_ramp_node.elements.new(cur_pos) 212 | cur_pos += step 213 | 214 | for i, color in enumerate(colors): 215 | color_ramp_node.elements[i].color = color 216 | 217 | 218 | def get_color_palette(): 219 | # https://www.colourlovers.com/palette/2943292 220 | # palette = ['#D7CEA3FF', '#907826FF', '#A46719FF', '#CE3F0EFF', '#1A0C47FF'] 221 | 222 | palette = [ 223 | [0.83984375, 0.8046875, 0.63671875, 1.0], 224 | [0.5625, 0.46875, 0.1484375, 1.0], 225 | [0.640625, 0.40234375, 0.09765625, 1.0], 226 | [0.8046875, 0.24609375, 0.0546875, 1.0], 227 | [0.1015625, 0.046875, 0.27734375, 1.0], 228 | ] 229 | 230 | return palette 231 | 232 | 233 | def apply_location(): 234 | bpy.ops.object.transform_apply(location=True) 235 | 236 | 237 | def setup_scene(): 238 | fps = 30 239 | loop_seconds = 6 240 | frame_count = fps * loop_seconds 241 | 242 | project_name = "color_slices" 243 | bpy.context.scene.render.image_settings.file_format = "PNG" 244 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/" 245 | 246 | seed = 0 247 | if seed: 248 | random.seed(seed) 249 | else: 250 | time_seed() 251 | 252 | # Utility Building Blocks 253 | clean_scene() 254 | set_scene_props(fps, loop_seconds) 255 | 256 | loc = (0, 0, 5) 257 | rot = (0, 0, 0) 258 | setup_camera(loc, rot) 259 | 260 | context = { 261 | "frame_count": frame_count, 262 | } 263 | 264 | context["colors"] = get_color_palette() 265 | 266 | return context 267 | 268 | 269 | ################################################################ 270 | # helper functions END 271 | ################################################################ 272 | 273 | 274 | def gen_perlin_curve(): 275 | 276 | bpy.ops.mesh.primitive_circle_add(vertices=512, radius=1) 277 | circle = active_object() 278 | 279 | deform_coords = [] 280 | 281 | for vert in circle.data.vertices: 282 | new_location = vert.co 283 | noise_value = mathutils.noise.noise(new_location) 284 | noise_value = noise_value / 2 285 | 286 | deform_vector = vert.co * noise_value 287 | 288 | deform_coord = vert.co + deform_vector 289 | deform_coords.append(deform_coord) 290 | 291 | bpy.ops.object.convert(target="CURVE") 292 | curve_obj = active_object() 293 | 294 | curve_obj.data.bevel_mode = "OBJECT" # remove this for Blender 2.8 295 | curve_obj.data.bevel_object = create_bevel_object() 296 | 297 | shape_key = add_shape_key(curve_obj, deform_coords) 298 | 299 | 300 | def add_shape_key(curve_obj, deform_coords): 301 | 302 | curve_obj.shape_key_add(name="Basis") 303 | 304 | shape_key = curve_obj.shape_key_add(name="Deform") 305 | 306 | deform_coords.reverse() 307 | for i, coord in enumerate(deform_coords): 308 | shape_key.data[i].co = coord 309 | shape_key.value = 1 310 | 311 | return shape_key 312 | 313 | 314 | def create_bevel_object(): 315 | bpy.ops.mesh.primitive_plane_add() 316 | bpy.ops.object.convert(target="CURVE") 317 | 318 | bevel_obj = active_object() 319 | bevel_obj.scale.x = 0.26 320 | bevel_obj.scale.y = 0.05 321 | bevel_obj.name = "bevel_object" 322 | 323 | return bevel_obj 324 | 325 | 326 | def main(): 327 | """ 328 | Python code for this art project 329 | https://www.artstation.com/artwork/48wX6L 330 | 331 | Tutorial Video about this script: 332 | https://www.youtube.com/watch?v=5cYn5WuyiWI&list=PLB8-FQgROBmlzQ7Xkq4YU7u08Zh3iuyPD&index=4 333 | """ 334 | context = setup_scene() 335 | 336 | gen_perlin_curve() 337 | 338 | 339 | if __name__ == "__main__": 340 | main() 341 | -------------------------------------------------------------------------------- /color_slices/color_slices_part3_start.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/5cYn5WuyiWI 3 | """ 4 | import random 5 | import time 6 | import math 7 | 8 | import bpy 9 | import mathutils 10 | 11 | ################################################################ 12 | # helper functions BEGIN 13 | ################################################################ 14 | 15 | 16 | def purge_orphans(): 17 | """ 18 | Remove all orphan data blocks 19 | 20 | see this from more info: 21 | https://youtu.be/3rNqVPtbhzc?t=149 22 | """ 23 | if bpy.app.version >= (3, 0, 0): 24 | # run this only for Blender versions 3.0 and higher 25 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 26 | else: 27 | # run this only for Blender versions lower than 3.0 28 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 29 | result = bpy.ops.outliner.orphans_purge() 30 | if result.pop() != "CANCELLED": 31 | purge_orphans() 32 | 33 | 34 | def clean_scene(): 35 | """ 36 | Removing all of the objects, collection, materials, particles, 37 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 38 | 39 | Checkout this video explanation with example 40 | 41 | "How to clean the scene with Python in Blender (with examples)" 42 | https://youtu.be/3rNqVPtbhzc 43 | """ 44 | # make sure the active object is not in Edit Mode 45 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 46 | bpy.ops.object.editmode_toggle() 47 | 48 | # make sure non of the objects are hidden from the viewport, selection, or disabled 49 | for obj in bpy.data.objects: 50 | obj.hide_set(False) 51 | obj.hide_select = False 52 | obj.hide_viewport = False 53 | 54 | # select all the object and delete them (just like pressing A + X + D in the viewport) 55 | bpy.ops.object.select_all(action="SELECT") 56 | bpy.ops.object.delete() 57 | 58 | # find all the collections and remove them 59 | collection_names = [col.name for col in bpy.data.collections] 60 | for name in collection_names: 61 | bpy.data.collections.remove(bpy.data.collections[name]) 62 | 63 | # in the case when you modify the world shader 64 | # delete and recreate the world object 65 | world_names = [world.name for world in bpy.data.worlds] 66 | for name in world_names: 67 | bpy.data.worlds.remove(bpy.data.worlds[name]) 68 | # create a new world data block 69 | bpy.ops.world.new() 70 | bpy.context.scene.world = bpy.data.worlds["World"] 71 | 72 | purge_orphans() 73 | 74 | 75 | def get_random_pallet_color(context): 76 | return random.choice(context["colors"]) 77 | 78 | 79 | def active_object(): 80 | """ 81 | returns the active object 82 | """ 83 | return bpy.context.active_object 84 | 85 | 86 | def time_seed(): 87 | """ 88 | Sets the random seed based on the time 89 | and copies the seed into the clipboard 90 | """ 91 | seed = time.time() 92 | print(f"seed: {seed}") 93 | random.seed(seed) 94 | 95 | # add the seed value to your clipboard 96 | bpy.context.window_manager.clipboard = str(seed) 97 | 98 | return seed 99 | 100 | 101 | def add_ctrl_empty(name=None): 102 | 103 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 104 | empty_ctrl = active_object() 105 | 106 | if name: 107 | empty_ctrl.name = name 108 | else: 109 | empty_ctrl.name = "empty.cntrl" 110 | 111 | return empty_ctrl 112 | 113 | 114 | def apply_material(material): 115 | obj = active_object() 116 | obj.data.materials.append(material) 117 | 118 | 119 | def make_active(obj): 120 | bpy.ops.object.select_all(action="DESELECT") 121 | obj.select_set(True) 122 | bpy.context.view_layer.objects.active = obj 123 | 124 | 125 | def track_empty(obj): 126 | """ 127 | create an empty and add a 'Track To' constraint 128 | """ 129 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 130 | 131 | make_active(obj) 132 | bpy.ops.object.constraint_add(type="TRACK_TO") 133 | bpy.context.object.constraints["Track To"].target = empty 134 | 135 | return empty 136 | 137 | 138 | def setup_camera(loc, rot): 139 | """ 140 | create and setup the camera 141 | """ 142 | bpy.ops.object.camera_add(location=loc, rotation=rot) 143 | camera = active_object() 144 | 145 | # set the camera as the "active camera" in the scene 146 | bpy.context.scene.camera = camera 147 | 148 | # set the Focal Length of the camera 149 | camera.data.lens = 70 150 | 151 | camera.data.passepartout_alpha = 0.9 152 | 153 | empty = track_empty(camera) 154 | 155 | return empty 156 | 157 | 158 | def set_1k_square_render_res(): 159 | """ 160 | Set the resolution of the rendered image to 1080 by 1080 161 | """ 162 | bpy.context.scene.render.resolution_x = 1080 163 | bpy.context.scene.render.resolution_y = 1080 164 | 165 | 166 | def set_scene_props(fps, loop_seconds): 167 | """ 168 | Set scene properties 169 | """ 170 | frame_count = fps * loop_seconds 171 | 172 | scene = bpy.context.scene 173 | scene.frame_end = frame_count 174 | 175 | # set the world background to black 176 | world = bpy.data.worlds["World"] 177 | if "Background" in world.node_tree.nodes: 178 | world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) 179 | 180 | scene.render.fps = fps 181 | 182 | scene.frame_current = 1 183 | scene.frame_start = 1 184 | 185 | scene.eevee.use_bloom = True 186 | scene.eevee.bloom_intensity = 0.005 187 | 188 | # set Ambient Occlusion properties 189 | scene.eevee.use_gtao = True 190 | scene.eevee.gtao_distance = 4 191 | scene.eevee.gtao_factor = 5 192 | 193 | scene.eevee.taa_render_samples = 64 194 | 195 | scene.view_settings.look = "Very High Contrast" 196 | 197 | set_1k_square_render_res() 198 | 199 | 200 | def make_ramp_from_colors(colors, color_ramp_node): 201 | """ 202 | Creates new sliders on a Color Ramp Node and 203 | applies the list of colors on each slider 204 | """ 205 | color_count = len(colors) 206 | 207 | step = 1 / color_count 208 | cur_pos = step 209 | # -2 is for the two sliders that are present on the ramp 210 | for _ in range(color_count - 2): 211 | color_ramp_node.elements.new(cur_pos) 212 | cur_pos += step 213 | 214 | for i, color in enumerate(colors): 215 | color_ramp_node.elements[i].color = color 216 | 217 | 218 | def get_color_palette(): 219 | # https://www.colourlovers.com/palette/2943292 220 | # palette = ['#D7CEA3FF', '#907826FF', '#A46719FF', '#CE3F0EFF', '#1A0C47FF'] 221 | 222 | palette = [ 223 | [0.83984375, 0.8046875, 0.63671875, 1.0], 224 | [0.5625, 0.46875, 0.1484375, 1.0], 225 | [0.640625, 0.40234375, 0.09765625, 1.0], 226 | [0.8046875, 0.24609375, 0.0546875, 1.0], 227 | [0.1015625, 0.046875, 0.27734375, 1.0], 228 | ] 229 | 230 | return palette 231 | 232 | 233 | def apply_location(): 234 | bpy.ops.object.transform_apply(location=True) 235 | 236 | 237 | def setup_scene(): 238 | fps = 30 239 | loop_seconds = 6 240 | frame_count = fps * loop_seconds 241 | 242 | project_name = "color_slices" 243 | bpy.context.scene.render.image_settings.file_format = "PNG" 244 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/" 245 | 246 | seed = 0 247 | if seed: 248 | random.seed(seed) 249 | else: 250 | time_seed() 251 | 252 | # Utility Building Blocks 253 | clean_scene() 254 | set_scene_props(fps, loop_seconds) 255 | 256 | loc = (0, 0, 5) 257 | rot = (0, 0, 0) 258 | setup_camera(loc, rot) 259 | 260 | context = { 261 | "frame_count": frame_count, 262 | } 263 | 264 | context["colors"] = get_color_palette() 265 | 266 | return context 267 | 268 | 269 | ################################################################ 270 | # helper functions END 271 | ################################################################ 272 | 273 | 274 | def gen_perlin_curve(): 275 | 276 | bpy.ops.mesh.primitive_circle_add(vertices=512, radius=1) 277 | circle = active_object() 278 | 279 | deform_coords = [] 280 | 281 | for vert in circle.data.vertices: 282 | new_location = vert.co 283 | noise_value = mathutils.noise.noise(new_location) 284 | noise_value = noise_value / 2 285 | 286 | deform_vector = vert.co * noise_value 287 | 288 | deform_coord = vert.co + deform_vector 289 | deform_coords.append(deform_coord) 290 | 291 | bpy.ops.object.convert(target="CURVE") 292 | curve_obj = active_object() 293 | 294 | 295 | def main(): 296 | """ 297 | Python code for this art project 298 | https://www.artstation.com/artwork/48wX6L 299 | 300 | Tutorial Video about this script: 301 | https://www.youtube.com/watch?v=5cYn5WuyiWI&list=PLB8-FQgROBmlzQ7Xkq4YU7u08Zh3iuyPD&index=4 302 | """ 303 | context = setup_scene() 304 | 305 | gen_perlin_curve() 306 | 307 | 308 | if __name__ == "__main__": 309 | main() 310 | -------------------------------------------------------------------------------- /compositor_import_image_sequence/compositor_import_image_sequence_done.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pathlib 3 | import pprint 4 | 5 | import bpy 6 | 7 | 8 | def remove_compositor_nodes(): 9 | bpy.context.scene.node_tree.nodes.clear() 10 | 11 | 12 | def get_image_files(image_folder_path, image_extension=".png"): 13 | image_files = list() 14 | for file_name in os.listdir(image_folder_path): 15 | if file_name.endswith(image_extension): 16 | image_files.append(file_name) 17 | image_files.sort() 18 | 19 | pprint.pprint(image_files) 20 | 21 | return image_files 22 | 23 | 24 | def add_compositor_nodes(image_sequence, duration): 25 | """ 26 | Find the Compositor Nodes we need here 27 | https://docs.blender.org/api/current/bpy.types.CompositorNode.html#bpy.types.CompositorNode 28 | """ 29 | scene = bpy.context.scene 30 | compositor_node_tree = scene.node_tree 31 | 32 | image_node = compositor_node_tree.nodes.new(type="CompositorNodeImage") 33 | image_node.image = image_sequence 34 | image_node.frame_duration = duration 35 | 36 | composite_node = compositor_node_tree.nodes.new(type="CompositorNodeComposite") 37 | composite_node.location.x = 200 38 | 39 | viewer_node = compositor_node_tree.nodes.new(type="CompositorNodeViewer") 40 | viewer_node.location.x = 200 41 | viewer_node.location.y = -200 42 | 43 | # create links 44 | compositor_node_tree.links.new(image_node.outputs["Image"], composite_node.inputs["Image"]) 45 | compositor_node_tree.links.new(image_node.outputs["Image"], viewer_node.inputs["Image"]) 46 | 47 | 48 | def import_image_sequence_into_compositor(image_folder_path, fps): 49 | image_files = get_image_files(image_folder_path) 50 | 51 | file_info = list() 52 | for image_name in image_files: 53 | file_info.append({"name": image_name}) 54 | 55 | bpy.ops.image.open(directory=image_folder_path, files=file_info) 56 | 57 | scene = bpy.context.scene 58 | scene.use_nodes = True 59 | 60 | remove_compositor_nodes() 61 | 62 | image_data_name = image_files[0] 63 | image_sequence = bpy.data.images[image_data_name] 64 | duration = len(image_files) 65 | add_compositor_nodes(image_sequence, duration) 66 | 67 | scene.frame_end = duration 68 | width, height = image_sequence.size 69 | scene.render.resolution_y = height 70 | scene.render.resolution_x = width 71 | scene.render.fps = fps 72 | 73 | 74 | def main(): 75 | """ 76 | Python code to import a folder with png(s) into the compositor as a sequence of images. 77 | """ 78 | image_folder_path = str(pathlib.Path.home() / "tmp" / "my_project") 79 | fps = 30 80 | import_image_sequence_into_compositor(image_folder_path, fps) 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /compositor_import_image_sequence/compositor_import_image_sequence_start.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import bpy 4 | 5 | 6 | def import_image_sequence_into_compositor(image_folder_path): 7 | pass 8 | 9 | 10 | def main(): 11 | """ 12 | Python code to import a folder with png(s) into the compositor as a sequence of images. 13 | """ 14 | image_folder_path = str(pathlib.Path.home() / "tmp" / "my_project") 15 | import_image_sequence_into_compositor(image_folder_path) 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /cube_loop/cube_loop_start.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import math 4 | 5 | import bpy 6 | 7 | ################################################################ 8 | # helper functions BEGIN 9 | ################################################################ 10 | 11 | 12 | def purge_orphans(): 13 | """ 14 | Remove all orphan data blocks 15 | 16 | see this from more info: 17 | https://youtu.be/3rNqVPtbhzc?t=149 18 | """ 19 | if bpy.app.version >= (3, 0, 0): 20 | # run this only for Blender versions 3.0 and higher 21 | bpy.ops.outliner.orphans_purge( 22 | do_local_ids=True, do_linked_ids=True, do_recursive=True 23 | ) 24 | else: 25 | # run this only for Blender versions lower than 3.0 26 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 27 | result = bpy.ops.outliner.orphans_purge() 28 | if result.pop() != "CANCELLED": 29 | purge_orphans() 30 | 31 | 32 | def clean_scene(): 33 | """ 34 | Removing all of the objects, collection, materials, particles, 35 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 36 | 37 | Checkout this video explanation with example 38 | 39 | "How to clean the scene with Python in Blender (with examples)" 40 | https://youtu.be/3rNqVPtbhzc 41 | """ 42 | # make sure the active object is not in Edit Mode 43 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 44 | bpy.ops.object.editmode_toggle() 45 | 46 | # make sure non of the objects are hidden from the viewport, selection, or disabled 47 | for obj in bpy.data.objects: 48 | obj.hide_set(False) 49 | obj.hide_select = False 50 | obj.hide_viewport = False 51 | 52 | # select all the object and delete them (just like pressing A + X + D in the viewport) 53 | bpy.ops.object.select_all(action="SELECT") 54 | bpy.ops.object.delete() 55 | 56 | # find all the collections and remove them 57 | collection_names = [col.name for col in bpy.data.collections] 58 | for name in collection_names: 59 | bpy.data.collections.remove(bpy.data.collections[name]) 60 | 61 | # in the case when you modify the world shader 62 | # delete and recreate the world object 63 | world_names = [world.name for world in bpy.data.worlds] 64 | for name in world_names: 65 | bpy.data.worlds.remove(bpy.data.worlds[name]) 66 | # create a new world data block 67 | bpy.ops.world.new() 68 | bpy.context.scene.world = bpy.data.worlds["World"] 69 | 70 | purge_orphans() 71 | 72 | 73 | def active_object(): 74 | """ 75 | returns the active object 76 | """ 77 | return bpy.context.active_object 78 | 79 | 80 | def time_seed(): 81 | """ 82 | Sets the random seed based on the time 83 | and copies the seed into the clipboard 84 | """ 85 | seed = time.time() 86 | print(f"seed: {seed}") 87 | random.seed(seed) 88 | 89 | # add the seed value to your clipboard 90 | bpy.context.window_manager.clipboard = str(seed) 91 | 92 | return seed 93 | 94 | 95 | def add_ctrl_empty(name=None): 96 | 97 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 98 | empty_ctrl = active_object() 99 | 100 | if name: 101 | empty_ctrl.name = name 102 | else: 103 | empty_ctrl.name = "empty.cntrl" 104 | 105 | return empty_ctrl 106 | 107 | 108 | def make_active(obj): 109 | bpy.ops.object.select_all(action="DESELECT") 110 | obj.select_set(True) 111 | bpy.context.view_layer.objects.active = obj 112 | 113 | 114 | def track_empty(obj): 115 | """ 116 | create an empty and add a 'Track To' constraint 117 | """ 118 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 119 | 120 | make_active(obj) 121 | bpy.ops.object.constraint_add(type="TRACK_TO") 122 | bpy.context.object.constraints["Track To"].target = empty 123 | 124 | return empty 125 | 126 | 127 | def setup_camera(loc, rot): 128 | """ 129 | create and setup the camera 130 | """ 131 | bpy.ops.object.camera_add(location=loc, rotation=rot) 132 | camera = active_object() 133 | 134 | # set the camera as the "active camera" in the scene 135 | bpy.context.scene.camera = camera 136 | 137 | # set the Focal Length of the camera 138 | camera.data.lens = 70 139 | 140 | camera.data.passepartout_alpha = 0.9 141 | 142 | 143 | def set_1k_square_render_res(): 144 | """ 145 | Set the resolution of the rendered image to 1080 by 1080 146 | """ 147 | bpy.context.scene.render.resolution_x = 1080 148 | bpy.context.scene.render.resolution_y = 1080 149 | 150 | 151 | def set_scene_props(fps, loop_seconds): 152 | """ 153 | Set scene properties 154 | """ 155 | frame_count = fps * loop_seconds 156 | 157 | scene = bpy.context.scene 158 | scene.frame_end = frame_count 159 | 160 | # set the world background to black 161 | world = bpy.data.worlds["World"] 162 | if "Background" in world.node_tree.nodes: 163 | world.node_tree.nodes["Background"].inputs["Color"].default_value = (0, 0, 0, 1) 164 | 165 | scene.render.fps = fps 166 | 167 | scene.frame_current = 1 168 | scene.frame_start = 1 169 | 170 | scene.eevee.use_bloom = True 171 | scene.eevee.bloom_intensity = 0.005 172 | 173 | # set Ambient Occlusion properties 174 | scene.eevee.use_gtao = True 175 | scene.eevee.gtao_distance = 4 176 | scene.eevee.gtao_factor = 5 177 | 178 | scene.eevee.taa_render_samples = 64 179 | 180 | scene.view_settings.look = "Very High Contrast" 181 | 182 | set_1k_square_render_res() 183 | 184 | 185 | def setup_scene(i=0): 186 | fps = 30 187 | loop_seconds = 12 188 | frame_count = fps * loop_seconds 189 | 190 | project_name = "cube_loops" 191 | bpy.context.scene.render.image_settings.file_format = "FFMPEG" 192 | bpy.context.scene.render.ffmpeg.format = "MPEG4" 193 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/loop_{i}.mp4" 194 | 195 | seed = 0 196 | if seed: 197 | random.seed(seed) 198 | else: 199 | time_seed() 200 | 201 | # Utility Building Blocks 202 | clean_scene() 203 | set_scene_props(fps, loop_seconds) 204 | 205 | loc = (0, 0, 15) 206 | rot = (0, 0, 0) 207 | setup_camera(loc, rot) 208 | 209 | context = { 210 | "frame_count": frame_count, 211 | } 212 | 213 | return context 214 | 215 | 216 | def make_fcurves_linear(): 217 | for fc in bpy.context.active_object.animation_data.action.fcurves: 218 | fc.extrapolation = "LINEAR" 219 | 220 | 221 | def get_random_color(): 222 | return random.choice( 223 | [ 224 | [0.92578125, 1, 0.0, 1], 225 | [0.203125, 0.19140625, 0.28125, 1], 226 | [0.8359375, 0.92578125, 0.08984375, 1], 227 | [0.16796875, 0.6796875, 0.3984375, 1], 228 | [0.6875, 0.71875, 0.703125, 1], 229 | [0.9609375, 0.9140625, 0.48046875, 1], 230 | [0.79296875, 0.8046875, 0.56640625, 1], 231 | [0.96484375, 0.8046875, 0.83984375, 1], 232 | [0.91015625, 0.359375, 0.125, 1], 233 | [0.984375, 0.4609375, 0.4140625, 1], 234 | [0.0625, 0.09375, 0.125, 1], 235 | [0.2578125, 0.9140625, 0.86328125, 1], 236 | [0.97265625, 0.21875, 0.1328125, 1], 237 | [0.87109375, 0.39453125, 0.53515625, 1], 238 | [0.8359375, 0.92578125, 0.08984375, 1], 239 | [0.37109375, 0.29296875, 0.54296875, 1], 240 | [0.984375, 0.4609375, 0.4140625, 1], 241 | [0.92578125, 0.16796875, 0.19921875, 1], 242 | [0.9375, 0.9609375, 0.96484375, 1], 243 | [0.3359375, 0.45703125, 0.4453125, 1], 244 | ] 245 | ) 246 | 247 | 248 | def apply_material(obj): 249 | color = get_random_color() 250 | mat = bpy.data.materials.new(name="Material") 251 | mat.use_nodes = True 252 | mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = color 253 | mat.node_tree.nodes["Principled BSDF"].inputs["Specular"].default_value = 0 254 | 255 | obj.data.materials.append(mat) 256 | 257 | 258 | def add_lights(): 259 | rot = (math.radians(60), 0, math.radians(120)) 260 | 261 | bpy.ops.object.light_add(type="SUN", rotation=rot) 262 | bpy.context.object.data.energy = 10 263 | bpy.context.object.data.angle = math.radians(180) 264 | bpy.context.object.data.use_shadow = False 265 | 266 | 267 | def render_loop(): 268 | bpy.ops.render.render(animation=True) 269 | 270 | 271 | ################################################################ 272 | # helper functions END 273 | ################################################################ 274 | 275 | 276 | def gen_centerpiece(context): 277 | pass 278 | 279 | 280 | def gen_background(): 281 | pass 282 | 283 | 284 | def main(): 285 | """ 286 | Python code to generate simple loop animations with cubes 287 | 288 | Tutorial: https://www.youtube.com/watch?v=GgX7rGcrHVI 289 | """ 290 | context = setup_scene() 291 | add_lights() 292 | gen_centerpiece(context) 293 | gen_background() 294 | # render_loop() 295 | 296 | 297 | if __name__ == "__main__": 298 | main() 299 | -------------------------------------------------------------------------------- /floret/script_done_bpybb.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python code to generate this animation 3 | https://www.artstation.com/artwork/0nVn4V 4 | 5 | Based on a phyllotaxis pattern created by formula 4.1 from 6 | http://algorithmicbotany.org/papers/abop/abop-ch4.pdf 7 | 8 | Inspired by Dan Shiffman's Coding Challenge #30: Phyllotaxis 9 | https://www.youtube.com/watch?v=KWoJgHFYWxY&t=0s 10 | 11 | """ 12 | 13 | import random 14 | import math 15 | 16 | import bpy 17 | 18 | from bpybb.color import hex_color_to_rgba 19 | from bpybb.material import create_emission_material 20 | from bpybb.animate import set_fcurve_extrapolation_to_linear 21 | from bpybb.object import track_empty 22 | from bpybb.output import set_1080px_square_render_res 23 | from bpybb.random import time_seed 24 | from bpybb.utils import clean_scene, active_object 25 | 26 | 27 | ################################################################ 28 | # helper functions BEGIN 29 | ################################################################ 30 | 31 | def get_random_color(): 32 | hex_color = random.choice( 33 | [ 34 | "#FC766A", 35 | "#5B84B1", 36 | "#5F4B8B", 37 | "#E69A8D", 38 | "#42EADD", 39 | "#CDB599", 40 | "#00A4CC", 41 | "#F95700", 42 | "#00203F", 43 | "#ADEFD1", 44 | "#606060", 45 | "#D6ED17", 46 | "#ED2B33", 47 | "#D85A7F", 48 | ] 49 | ) 50 | 51 | return hex_color_to_rgba(hex_color) 52 | 53 | 54 | def setup_camera(loc, rot): 55 | """ 56 | create and setup the camera 57 | """ 58 | bpy.ops.object.camera_add(location=loc, rotation=rot) 59 | camera = active_object() 60 | 61 | # set the camera as the "active camera" in the scene 62 | bpy.context.scene.camera = camera 63 | 64 | # set the Focal Length of the camera 65 | camera.data.lens = 70 66 | 67 | camera.data.passepartout_alpha = 0.9 68 | 69 | empty = track_empty(camera) 70 | 71 | bpy.context.object.data.dof.use_dof = True 72 | bpy.context.object.data.dof.aperture_fstop = 0.1 73 | 74 | return empty 75 | 76 | 77 | def set_scene_props(fps, loop_seconds): 78 | """ 79 | Set scene properties 80 | """ 81 | frame_count = fps * loop_seconds 82 | 83 | scene = bpy.context.scene 84 | scene.frame_end = frame_count 85 | 86 | # set the world background to black 87 | world = bpy.data.worlds["World"] 88 | if "Background" in world.node_tree.nodes: 89 | world.node_tree.nodes["Background"].inputs["Color"].default_value = (0, 0, 0, 1) 90 | 91 | scene.render.fps = fps 92 | 93 | scene.frame_current = 1 94 | scene.frame_start = 1 95 | 96 | bpy.context.scene.eevee.use_bloom = True 97 | 98 | scene.view_settings.look = "Very High Contrast" 99 | 100 | set_1080px_square_render_res() 101 | 102 | 103 | def scene_setup(i=0): 104 | fps = 30 105 | loop_seconds = 12 106 | frame_count = fps * loop_seconds 107 | 108 | project_name = "floret" 109 | bpy.context.scene.render.image_settings.file_format = "FFMPEG" 110 | bpy.context.scene.render.ffmpeg.format = "MPEG4" 111 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/loop_{i}.mp4" 112 | 113 | seed = 0 114 | if seed: 115 | random.seed(seed) 116 | else: 117 | time_seed() 118 | 119 | # Utility Building Blocks 120 | clean_scene() 121 | 122 | set_scene_props(fps, loop_seconds) 123 | 124 | loc = (0, 0, 80) 125 | rot = (0, 0, 0) 126 | setup_camera(loc, rot) 127 | 128 | context = { 129 | "frame_count": frame_count, 130 | "fps": fps, 131 | } 132 | 133 | return context 134 | 135 | 136 | def create_data_animation_loop(obj, data_path, start_value, mid_value, start_frame, loop_length, linear_extrapolation=True): 137 | """ 138 | To make a data property loop we need to: 139 | 1. set the property to an initial value and add a keyframe in the beginning of the loop 140 | 2. set the property to a middle value and add a keyframe in the middle of the loop 141 | 3. set the property the initial value and add a keyframe at the end of the loop 142 | """ 143 | 144 | # set the start value 145 | setattr(obj, data_path, start_value) 146 | # add a keyframe at the start 147 | obj.keyframe_insert(data_path, frame=start_frame) 148 | 149 | # set the middle value 150 | setattr(obj, data_path, mid_value) 151 | # add a keyframe in the middle 152 | mid_frame = start_frame + (loop_length) / 2 153 | obj.keyframe_insert(data_path, frame=mid_frame) 154 | 155 | # set the end value 156 | setattr(obj, data_path, start_value) 157 | # add a keyframe in the end 158 | end_frame = start_frame + loop_length 159 | obj.keyframe_insert(data_path, frame=end_frame) 160 | 161 | if linear_extrapolation: 162 | set_fcurve_extrapolation_to_linear() 163 | 164 | 165 | ################################################################ 166 | # helper functions END 167 | ################################################################ 168 | 169 | 170 | def calculate_end_frame(context, current_frame): 171 | # make sure the end frame is divisible by the FPS 172 | quotient, remainder = divmod(current_frame, context["fps"]) 173 | 174 | if remainder != 0: 175 | bpy.context.scene.frame_end = (quotient + 1) * context["fps"] 176 | else: 177 | bpy.context.scene.frame_end = current_frame 178 | 179 | return bpy.context.scene.frame_end 180 | 181 | 182 | def animate_depth_of_field(frame_end): 183 | 184 | start_focus_distance = 15.0 185 | mid_focus_distance = bpy.data.objects["Camera"].location.z / 2 186 | start_frame = 1 187 | loop_length = frame_end 188 | create_data_animation_loop( 189 | bpy.data.objects["Camera"].data.dof, 190 | "focus_distance", 191 | start_focus_distance, 192 | mid_focus_distance, 193 | start_frame, 194 | loop_length, 195 | linear_extrapolation=False, 196 | ) 197 | 198 | 199 | def calculate_phyllotaxis_coordinates(n, angle, scale_fac): 200 | """ 201 | calculating a point in a phyllotaxis pattern based on formula 4.1 from 202 | http://algorithmicbotany.org/papers/abop/abop-ch4.pdf 203 | 204 | See tutorial for detailed description: https://youtu.be/aeDbYuJyXr8 205 | """ 206 | # calculate "φ" in formula (4.1) http://algorithmicbotany.org/papers/abop/abop-ch4.pdf 207 | current_angle = n * angle 208 | 209 | # calculate "r" in formula (4.1) http://algorithmicbotany.org/papers/abop/abop-ch4.pdf 210 | current_radius = scale_fac * math.sqrt(n) 211 | 212 | # convert from Polar Coordinates (r,φ) to Cartesian Coordinates (x,y) 213 | x = current_radius * math.cos(current_angle) 214 | y = current_radius * math.sin(current_angle) 215 | 216 | return x, y 217 | 218 | 219 | def create_centerpiece(context): 220 | 221 | colors = (hex_color_to_rgba("#306998"), hex_color_to_rgba("#FFD43B")) 222 | 223 | ico_sphere_radius = 0.2 224 | 225 | # "c" in formula (4.1) http://algorithmicbotany.org/papers/abop/abop-ch4.pdf 226 | scale_fac = 1.0 227 | 228 | # "α" angle in radians in formula (4.1) http://algorithmicbotany.org/papers/abop/abop-ch4.pdf 229 | angle = math.radians(random.uniform(137.0, 138.0)) 230 | 231 | # set angle to the Fibonacci angle 137.5 to get the sunflower pattern 232 | # angle = math.radians(137.5) 233 | 234 | current_frame = 1 235 | frame_step = 0.5 236 | start_emission_strength_value = 0 237 | mid_emission_strength_value = 20 238 | loop_length = 60 239 | 240 | count = 300 241 | for n in range(count): 242 | 243 | x, y = calculate_phyllotaxis_coordinates(n, angle, scale_fac) 244 | 245 | # place ico sphere 246 | bpy.ops.mesh.primitive_ico_sphere_add(radius=ico_sphere_radius, location=(x, y, 0)) 247 | obj = active_object() 248 | 249 | # assign an emission material 250 | material, nodes = create_emission_material(color=random.choice(colors), name=f"{n}_sphr", energy=30, return_nodes=True) 251 | obj.data.materials.append(material) 252 | 253 | # animate the Strength value of the emission material 254 | create_data_animation_loop( 255 | nodes["Emission"].inputs["Strength"], 256 | "default_value", 257 | start_emission_strength_value, 258 | mid_emission_strength_value, 259 | current_frame, 260 | loop_length, 261 | linear_extrapolation=False, 262 | ) 263 | 264 | current_frame += frame_step 265 | 266 | current_frame = int(current_frame + loop_length) 267 | end_frame = calculate_end_frame(context, current_frame) 268 | 269 | animate_depth_of_field(end_frame) 270 | 271 | 272 | def main(): 273 | """ 274 | Python code to generate this animation 275 | https://www.artstation.com/artwork/0nVn4V 276 | """ 277 | context = scene_setup() 278 | create_centerpiece(context) 279 | 280 | 281 | if __name__ == "__main__": 282 | main() 283 | -------------------------------------------------------------------------------- /flow_out/flow_out_start.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | import math 4 | 5 | import bpy 6 | 7 | ################################################################ 8 | # helper functions BEGIN 9 | ################################################################ 10 | 11 | 12 | def purge_orphans(): 13 | """ 14 | Remove all orphan data blocks 15 | 16 | see this from more info: 17 | https://youtu.be/3rNqVPtbhzc?t=149 18 | """ 19 | if bpy.app.version >= (3, 0, 0): 20 | # run this only for Blender versions 3.0 and higher 21 | bpy.ops.outliner.orphans_purge( 22 | do_local_ids=True, do_linked_ids=True, do_recursive=True 23 | ) 24 | else: 25 | # run this only for Blender versions lower than 3.0 26 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 27 | result = bpy.ops.outliner.orphans_purge() 28 | if result.pop() != "CANCELLED": 29 | purge_orphans() 30 | 31 | 32 | def clean_scene(): 33 | """ 34 | Removing all of the objects, collection, materials, particles, 35 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 36 | 37 | Checkout this video explanation with example 38 | 39 | "How to clean the scene with Python in Blender (with examples)" 40 | https://youtu.be/3rNqVPtbhzc 41 | """ 42 | # make sure the active object is not in Edit Mode 43 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 44 | bpy.ops.object.editmode_toggle() 45 | 46 | # make sure non of the objects are hidden from the viewport, selection, or disabled 47 | for obj in bpy.data.objects: 48 | obj.hide_set(False) 49 | obj.hide_select = False 50 | obj.hide_viewport = False 51 | 52 | # select all the object and delete them (just like pressing A + X + D in the viewport) 53 | bpy.ops.object.select_all(action="SELECT") 54 | bpy.ops.object.delete() 55 | 56 | # find all the collections and remove them 57 | collection_names = [col.name for col in bpy.data.collections] 58 | for name in collection_names: 59 | bpy.data.collections.remove(bpy.data.collections[name]) 60 | 61 | # in the case when you modify the world shader 62 | # delete and recreate the world object 63 | world_names = [world.name for world in bpy.data.worlds] 64 | for name in world_names: 65 | bpy.data.worlds.remove(bpy.data.worlds[name]) 66 | # create a new world data block 67 | bpy.ops.world.new() 68 | bpy.context.scene.world = bpy.data.worlds["World"] 69 | 70 | purge_orphans() 71 | 72 | 73 | def active_object(): 74 | """ 75 | returns the active object 76 | """ 77 | return bpy.context.active_object 78 | 79 | 80 | def time_seed(): 81 | """ 82 | Sets the random seed based on the time 83 | and copies the seed into the clipboard 84 | """ 85 | seed = time.time() 86 | print(f"seed: {seed}") 87 | random.seed(seed) 88 | 89 | # add the seed value to your clipboard 90 | bpy.context.window_manager.clipboard = str(seed) 91 | 92 | return seed 93 | 94 | 95 | def add_ctrl_empty(name=None): 96 | 97 | bpy.ops.object.empty_add() 98 | empty_ctrl = active_object() 99 | 100 | if name: 101 | empty_ctrl.name = name 102 | else: 103 | empty_ctrl.name = "empty.cntrl" 104 | 105 | return empty_ctrl 106 | 107 | 108 | def make_active(obj): 109 | bpy.ops.object.select_all(action="DESELECT") 110 | obj.select_set(True) 111 | bpy.context.view_layer.objects.active = obj 112 | 113 | 114 | def track_empty(obj): 115 | """ 116 | create an empty and add a 'Track To' constraint 117 | """ 118 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 119 | 120 | make_active(obj) 121 | bpy.ops.object.constraint_add(type="TRACK_TO") 122 | bpy.context.object.constraints["Track To"].target = empty 123 | 124 | return empty 125 | 126 | 127 | def setup_camera(loc, rot): 128 | """ 129 | create and setup the camera 130 | """ 131 | bpy.ops.object.camera_add(location=loc, rotation=rot) 132 | camera = active_object() 133 | 134 | # set the camera as the "active camera" in the scene 135 | bpy.context.scene.camera = camera 136 | 137 | # set the Focal Length of the camera 138 | camera.data.lens = 70 139 | 140 | camera.data.passepartout_alpha = 0.9 141 | 142 | empty = track_empty(camera) 143 | 144 | camera.data.dof.use_dof = True 145 | camera.data.dof.focus_object = empty 146 | camera.data.dof.aperture_fstop = 0.1 147 | 148 | return empty 149 | 150 | 151 | def set_1080px_square_render_res(): 152 | """ 153 | Set the resolution of the rendered image to 1080 by 1080 154 | """ 155 | bpy.context.scene.render.resolution_x = 1080 156 | bpy.context.scene.render.resolution_y = 1080 157 | 158 | 159 | def set_scene_props(fps, loop_seconds): 160 | """ 161 | Set scene properties 162 | """ 163 | frame_count = fps * loop_seconds 164 | 165 | scene = bpy.context.scene 166 | scene.frame_end = frame_count 167 | 168 | # set the world background to black 169 | world = bpy.data.worlds["World"] 170 | if "Background" in world.node_tree.nodes: 171 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 172 | 173 | scene.render.fps = fps 174 | 175 | scene.frame_current = 1 176 | scene.frame_start = 1 177 | 178 | scene.eevee.use_bloom = True 179 | scene.eevee.bloom_intensity = 0.005 180 | 181 | # set Ambient Occlusion properties 182 | scene.eevee.use_gtao = True 183 | scene.eevee.gtao_distance = 4 184 | scene.eevee.gtao_factor = 5 185 | 186 | scene.eevee.taa_render_samples = 64 187 | 188 | scene.view_settings.look = "Very High Contrast" 189 | bpy.context.preferences.edit.use_negative_frames = True 190 | 191 | set_1080px_square_render_res() 192 | 193 | 194 | def setup_scene(i=0): 195 | fps = 30 196 | loop_seconds = 6 197 | frame_count = fps * loop_seconds 198 | 199 | project_name = "outgoing_circles" 200 | bpy.context.scene.render.image_settings.file_format = "FFMPEG" 201 | bpy.context.scene.render.ffmpeg.format = "MPEG4" 202 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/loop_{i}.mp4" 203 | 204 | seed = 0 205 | if seed: 206 | random.seed(seed) 207 | else: 208 | time_seed() 209 | 210 | # Utility Building Blocks 211 | clean_scene() 212 | set_scene_props(fps, loop_seconds) 213 | 214 | loc = (0, 0, 7) 215 | rot = (0, 0, 0) 216 | setup_camera(loc, rot) 217 | 218 | context = { 219 | "frame_count": frame_count, 220 | } 221 | 222 | return context 223 | 224 | 225 | def make_fcurves_linear(): 226 | for fcurve in bpy.context.active_object.animation_data.action.fcurves: 227 | for points in fcurve.keyframe_points: 228 | points.interpolation = "LINEAR" 229 | 230 | 231 | def get_random_color(): 232 | return random.choice( 233 | [ 234 | [0.48046875, 0.171875, 0.5, 0.99609375], 235 | [0.3515625, 0.13671875, 0.39453125, 0.99609375], 236 | [0.2734375, 0.21484375, 0.08984375, 0.99609375], 237 | [0.5625, 0.45703125, 0.234375, 0.99609375], 238 | [0.92578125, 0.8828125, 0.77734375, 0.99609375], 239 | [0.1640625, 0.4921875, 0.13671875, 0.99609375], 240 | [0.453125, 0.74609375, 0.328125, 0.99609375], 241 | [0.2734375, 0.21484375, 0.08984375, 0.99609375], 242 | [0.5625, 0.45703125, 0.234375, 0.99609375], 243 | [0.92578125, 0.8828125, 0.77734375, 0.99609375], 244 | [0.1640625, 0.4921875, 0.13671875, 0.99609375], 245 | [0.453125, 0.74609375, 0.328125, 0.99609375], 246 | [0.00390625, 0.11328125, 0.15625, 0.99609375], 247 | [0.0234375, 0.49609375, 0.46875, 0.99609375], 248 | [0.01953125, 0.51953125, 0.6953125, 0.99609375], 249 | [0, 0.66796875, 0.78515625, 0.99609375], 250 | [0, 0.15234375, 0.171875, 0.99609375], 251 | [0.3203125, 0, 0.12890625, 0.99609375], 252 | [0.56640625, 0, 0.2265625, 0.99609375], 253 | [0.99609375, 0, 0.3984375, 0.99609375], 254 | [0.9453125, 0.640625, 0.33203125, 0.99609375], 255 | [0.51953125, 0.453125, 0.38671875, 0.99609375], 256 | [0.84765625, 0.94140625, 0.63671875, 0.99609375], 257 | [0.30859375, 0.91796875, 0.59375, 0.99609375], 258 | [0.46484375, 0.76171875, 0.47265625, 0.99609375], 259 | [0.71875, 0.5390625, 0.546875, 0.99609375], 260 | [0.40234375, 0.3671875, 0.30859375, 0.99609375], 261 | ] 262 | ) 263 | 264 | 265 | def apply_material(obj): 266 | color = get_random_color() 267 | mat = bpy.data.materials.new(name="Material") 268 | mat.use_nodes = True 269 | mat.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = color 270 | mat.node_tree.nodes["Principled BSDF"].inputs["Specular"].default_value = 0 271 | 272 | obj.data.materials.append(mat) 273 | 274 | 275 | def add_lights(): 276 | bpy.ops.object.light_add(type="SUN") 277 | bpy.context.object.data.energy = 10 278 | 279 | 280 | ################################################################ 281 | # helper functions END 282 | ################################################################ 283 | 284 | 285 | def gen_centerpiece(context): 286 | pass 287 | 288 | 289 | def main(): 290 | """ 291 | Python code to generate an animation loop with circles 292 | moving from the origin outward 293 | """ 294 | context = setup_scene() 295 | gen_centerpiece(context) 296 | add_lights() 297 | 298 | 299 | if __name__ == "__main__": 300 | main() 301 | -------------------------------------------------------------------------------- /geo_nodes/subdivided_triangulated_cube/subdivided_triangulated_cube_part_1_done.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/Is8Qu7onvzM 3 | """ 4 | 5 | import random 6 | import time 7 | 8 | import bpy 9 | 10 | 11 | ################################################################ 12 | # helper functions BEGIN 13 | ################################################################ 14 | 15 | 16 | def purge_orphans(): 17 | """ 18 | Remove all orphan data blocks 19 | 20 | see this from more info: 21 | https://youtu.be/3rNqVPtbhzc?t=149 22 | """ 23 | if bpy.app.version >= (3, 0, 0): 24 | # run this only for Blender versions 3.0 and higher 25 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 26 | else: 27 | # run this only for Blender versions lower than 3.0 28 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 29 | result = bpy.ops.outliner.orphans_purge() 30 | if result.pop() != "CANCELLED": 31 | purge_orphans() 32 | 33 | 34 | def clean_scene(): 35 | """ 36 | Removing all of the objects, collection, materials, particles, 37 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 38 | 39 | Checkout this video explanation with example 40 | 41 | "How to clean the scene with Python in Blender (with examples)" 42 | https://youtu.be/3rNqVPtbhzc 43 | """ 44 | # make sure the active object is not in Edit Mode 45 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 46 | bpy.ops.object.editmode_toggle() 47 | 48 | # make sure non of the objects are hidden from the viewport, selection, or disabled 49 | for obj in bpy.data.objects: 50 | obj.hide_set(False) 51 | obj.hide_select = False 52 | obj.hide_viewport = False 53 | 54 | # select all the object and delete them (just like pressing A + X + D in the viewport) 55 | bpy.ops.object.select_all(action="SELECT") 56 | bpy.ops.object.delete() 57 | 58 | # find all the collections and remove them 59 | collection_names = [col.name for col in bpy.data.collections] 60 | for name in collection_names: 61 | bpy.data.collections.remove(bpy.data.collections[name]) 62 | 63 | # in the case when you modify the world shader 64 | # delete and recreate the world object 65 | world_names = [world.name for world in bpy.data.worlds] 66 | for name in world_names: 67 | bpy.data.worlds.remove(bpy.data.worlds[name]) 68 | # create a new world data block 69 | bpy.ops.world.new() 70 | bpy.context.scene.world = bpy.data.worlds["World"] 71 | 72 | purge_orphans() 73 | 74 | 75 | def active_object(): 76 | """ 77 | returns the currently active object 78 | """ 79 | return bpy.context.active_object 80 | 81 | 82 | def time_seed(): 83 | """ 84 | Sets the random seed based on the time 85 | and copies the seed into the clipboard 86 | """ 87 | seed = time.time() 88 | print(f"seed: {seed}") 89 | random.seed(seed) 90 | 91 | # add the seed value to your clipboard 92 | bpy.context.window_manager.clipboard = str(seed) 93 | 94 | return seed 95 | 96 | 97 | def set_fcurve_extrapolation_to_linear(): 98 | for fc in bpy.context.active_object.animation_data.action.fcurves: 99 | fc.extrapolation = "LINEAR" 100 | 101 | 102 | def create_data_animation_loop(obj, data_path, start_value, mid_value, start_frame, loop_length, linear_extrapolation=True): 103 | """ 104 | To make a data property loop we need to: 105 | 1. set the property to an initial value and add a keyframe in the beginning of the loop 106 | 2. set the property to a middle value and add a keyframe in the middle of the loop 107 | 3. set the property the initial value and add a keyframe at the end of the loop 108 | """ 109 | # set the start value 110 | setattr(obj, data_path, start_value) 111 | # add a keyframe at the start 112 | obj.keyframe_insert(data_path, frame=start_frame) 113 | 114 | # set the middle value 115 | setattr(obj, data_path, mid_value) 116 | # add a keyframe in the middle 117 | mid_frame = start_frame + (loop_length) / 2 118 | obj.keyframe_insert(data_path, frame=mid_frame) 119 | 120 | # set the end value 121 | setattr(obj, data_path, start_value) 122 | # add a keyframe in the end 123 | end_frame = start_frame + loop_length 124 | obj.keyframe_insert(data_path, frame=end_frame) 125 | 126 | if linear_extrapolation: 127 | set_fcurve_extrapolation_to_linear() 128 | 129 | 130 | def set_scene_props(fps, frame_count): 131 | """ 132 | Set scene properties 133 | """ 134 | scene = bpy.context.scene 135 | scene.frame_end = frame_count 136 | 137 | # set the world background to black 138 | world = bpy.data.worlds["World"] 139 | if "Background" in world.node_tree.nodes: 140 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 141 | 142 | scene.render.fps = fps 143 | 144 | scene.frame_current = 1 145 | scene.frame_start = 1 146 | 147 | 148 | def scene_setup(): 149 | fps = 30 150 | loop_seconds = 12 151 | frame_count = fps * loop_seconds 152 | 153 | seed = 0 154 | if seed: 155 | random.seed(seed) 156 | else: 157 | time_seed() 158 | 159 | clean_scene() 160 | 161 | set_scene_props(fps, frame_count) 162 | 163 | 164 | ################################################################ 165 | # helper functions END 166 | ################################################################ 167 | 168 | 169 | def link_nodes_by_mesh_socket(node_tree, from_node, to_node): 170 | node_tree.links.new(from_node.outputs["Mesh"], to_node.inputs["Mesh"]) 171 | 172 | 173 | def create_node(node_tree, type_name, node_x_location, node_location_step_x=0): 174 | """Creates a node of a given type, and sets/updates the location of the node on the X axis. 175 | Returning the node object and the next location on the X axis for the next node. 176 | """ 177 | node_obj = node_tree.nodes.new(type=type_name) 178 | node_obj.location.x = node_x_location 179 | node_x_location += node_location_step_x 180 | 181 | return node_obj, node_x_location 182 | 183 | 184 | def update_geo_node_tree(node_tree): 185 | """ 186 | Adding a Cube Mesh, Subdiv, Triangulate, Edge Split, and Element scale geo node into the 187 | geo node tree 188 | 189 | Geo Node type names found here 190 | https://docs.blender.org/api/current/bpy.types.GeometryNode.html 191 | """ 192 | out_node = node_tree.nodes["Group Output"] 193 | 194 | node_x_location = 0 195 | node_location_step_x = 300 196 | 197 | mesh_cube_node, node_x_location = create_node(node_tree, "GeometryNodeMeshCube", node_x_location, node_location_step_x) 198 | 199 | subdivide_mesh_node, node_x_location = create_node(node_tree, "GeometryNodeSubdivideMesh", node_x_location, node_location_step_x) 200 | subdivide_mesh_node.inputs["Level"].default_value = 3 201 | 202 | triangulate_node, node_x_location = create_node(node_tree, "GeometryNodeTriangulate", node_x_location, node_location_step_x) 203 | 204 | split_edges_node, node_x_location = create_node(node_tree, "GeometryNodeSplitEdges", node_x_location, node_location_step_x) 205 | 206 | scale_elements_node, node_x_location = create_node(node_tree, "GeometryNodeScaleElements", node_x_location, node_location_step_x) 207 | scale_elements_node.inputs["Scale"].default_value = 0.8 208 | 209 | out_node.location.x = node_x_location 210 | 211 | link_nodes_by_mesh_socket(node_tree, from_node=mesh_cube_node, to_node=subdivide_mesh_node) 212 | link_nodes_by_mesh_socket(node_tree, from_node=subdivide_mesh_node, to_node=triangulate_node) 213 | link_nodes_by_mesh_socket(node_tree, from_node=triangulate_node, to_node=split_edges_node) 214 | 215 | from_node = split_edges_node 216 | to_node = scale_elements_node 217 | node_tree.links.new(from_node.outputs["Mesh"], to_node.inputs["Geometry"]) 218 | 219 | from_node = scale_elements_node 220 | to_node = out_node 221 | node_tree.links.new(from_node.outputs["Geometry"], to_node.inputs["Geometry"]) 222 | 223 | 224 | def create_centerpiece(): 225 | bpy.ops.mesh.primitive_plane_add() 226 | 227 | bpy.ops.node.new_geometry_nodes_modifier() 228 | node_tree = bpy.data.node_groups["Geometry Nodes"] 229 | 230 | update_geo_node_tree(node_tree) 231 | 232 | bpy.ops.object.modifier_add(type="SOLIDIFY") 233 | 234 | 235 | def main(): 236 | """ 237 | Python code to generate an animated geo nodes node tree 238 | that consists of a subdivided & triangulated cube with animated faces 239 | """ 240 | scene_setup() 241 | create_centerpiece() 242 | 243 | 244 | if __name__ == "__main__": 245 | main() 246 | -------------------------------------------------------------------------------- /geo_nodes/subdivided_triangulated_cube/subdivided_triangulated_cube_part_1_start.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/Is8Qu7onvzM 3 | """ 4 | import random 5 | import time 6 | 7 | import bpy 8 | 9 | 10 | ################################################################ 11 | # helper functions BEGIN 12 | ################################################################ 13 | 14 | 15 | def purge_orphans(): 16 | """ 17 | Remove all orphan data blocks 18 | 19 | see this from more info: 20 | https://youtu.be/3rNqVPtbhzc?t=149 21 | """ 22 | if bpy.app.version >= (3, 0, 0): 23 | # run this only for Blender versions 3.0 and higher 24 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 25 | else: 26 | # run this only for Blender versions lower than 3.0 27 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 28 | result = bpy.ops.outliner.orphans_purge() 29 | if result.pop() != "CANCELLED": 30 | purge_orphans() 31 | 32 | 33 | def clean_scene(): 34 | """ 35 | Removing all of the objects, collection, materials, particles, 36 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 37 | 38 | Checkout this video explanation with example 39 | 40 | "How to clean the scene with Python in Blender (with examples)" 41 | https://youtu.be/3rNqVPtbhzc 42 | """ 43 | # make sure the active object is not in Edit Mode 44 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 45 | bpy.ops.object.editmode_toggle() 46 | 47 | # make sure non of the objects are hidden from the viewport, selection, or disabled 48 | for obj in bpy.data.objects: 49 | obj.hide_set(False) 50 | obj.hide_select = False 51 | obj.hide_viewport = False 52 | 53 | # select all the object and delete them (just like pressing A + X + D in the viewport) 54 | bpy.ops.object.select_all(action="SELECT") 55 | bpy.ops.object.delete() 56 | 57 | # find all the collections and remove them 58 | collection_names = [col.name for col in bpy.data.collections] 59 | for name in collection_names: 60 | bpy.data.collections.remove(bpy.data.collections[name]) 61 | 62 | # in the case when you modify the world shader 63 | # delete and recreate the world object 64 | world_names = [world.name for world in bpy.data.worlds] 65 | for name in world_names: 66 | bpy.data.worlds.remove(bpy.data.worlds[name]) 67 | # create a new world data block 68 | bpy.ops.world.new() 69 | bpy.context.scene.world = bpy.data.worlds["World"] 70 | 71 | purge_orphans() 72 | 73 | 74 | def active_object(): 75 | """ 76 | returns the currently active object 77 | """ 78 | return bpy.context.active_object 79 | 80 | 81 | def time_seed(): 82 | """ 83 | Sets the random seed based on the time 84 | and copies the seed into the clipboard 85 | """ 86 | seed = time.time() 87 | print(f"seed: {seed}") 88 | random.seed(seed) 89 | 90 | # add the seed value to your clipboard 91 | bpy.context.window_manager.clipboard = str(seed) 92 | 93 | return seed 94 | 95 | 96 | def set_fcurve_extrapolation_to_linear(): 97 | for fc in bpy.context.active_object.animation_data.action.fcurves: 98 | fc.extrapolation = "LINEAR" 99 | 100 | 101 | def create_data_animation_loop(obj, data_path, start_value, mid_value, start_frame, loop_length, linear_extrapolation=True): 102 | """ 103 | To make a data property loop we need to: 104 | 1. set the property to an initial value and add a keyframe in the beginning of the loop 105 | 2. set the property to a middle value and add a keyframe in the middle of the loop 106 | 3. set the property the initial value and add a keyframe at the end of the loop 107 | """ 108 | # set the start value 109 | setattr(obj, data_path, start_value) 110 | # add a keyframe at the start 111 | obj.keyframe_insert(data_path, frame=start_frame) 112 | 113 | # set the middle value 114 | setattr(obj, data_path, mid_value) 115 | # add a keyframe in the middle 116 | mid_frame = start_frame + (loop_length) / 2 117 | obj.keyframe_insert(data_path, frame=mid_frame) 118 | 119 | # set the end value 120 | setattr(obj, data_path, start_value) 121 | # add a keyframe in the end 122 | end_frame = start_frame + loop_length 123 | obj.keyframe_insert(data_path, frame=end_frame) 124 | 125 | if linear_extrapolation: 126 | set_fcurve_extrapolation_to_linear() 127 | 128 | 129 | def set_scene_props(fps, frame_count): 130 | """ 131 | Set scene properties 132 | """ 133 | scene = bpy.context.scene 134 | scene.frame_end = frame_count 135 | 136 | # set the world background to black 137 | world = bpy.data.worlds["World"] 138 | if "Background" in world.node_tree.nodes: 139 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 140 | 141 | scene.render.fps = fps 142 | 143 | scene.frame_current = 1 144 | scene.frame_start = 1 145 | 146 | 147 | def scene_setup(): 148 | fps = 30 149 | loop_seconds = 12 150 | frame_count = fps * loop_seconds 151 | 152 | seed = 0 153 | if seed: 154 | random.seed(seed) 155 | else: 156 | time_seed() 157 | 158 | clean_scene() 159 | 160 | set_scene_props(fps, frame_count) 161 | 162 | 163 | ################################################################ 164 | # helper functions END 165 | ################################################################ 166 | 167 | 168 | def create_centerpiece(): 169 | pass 170 | 171 | 172 | def main(): 173 | """ 174 | Python code to generate an animated geo nodes node tree 175 | that consists of a subdivided & triangulated cube with animated faces 176 | """ 177 | scene_setup() 178 | create_centerpiece() 179 | 180 | 181 | if __name__ == "__main__": 182 | main() 183 | -------------------------------------------------------------------------------- /in_or_out/in_or_out_done_bpybb.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import bpy 4 | 5 | from bpybb.animate import animate_360_rotation 6 | from bpybb.color import hex_color_to_rgba 7 | from bpybb.material import create_reflective_material, apply_emission_material 8 | from bpybb.object import track_empty 9 | from bpybb.output import set_1080px_square_render_res 10 | from bpybb.random import time_seed, apply_random_rotation 11 | from bpybb.utils import clean_scene, active_object, Axis, clean_scene_experimental 12 | 13 | ################################################################ 14 | # helper functions BEGIN 15 | ################################################################ 16 | 17 | 18 | def get_random_color(): 19 | hex_color = random.choice( 20 | [ 21 | "#FC766A", 22 | "#5B84B1", 23 | "#5F4B8B", 24 | "#E69A8D", 25 | "#42EADD", 26 | "#CDB599", 27 | "#00A4CC", 28 | "#F95700", 29 | "#00203F", 30 | "#ADEFD1", 31 | "#606060", 32 | "#D6ED17", 33 | "#ED2B33", 34 | "#D85A7F", 35 | ] 36 | ) 37 | 38 | return hex_color_to_rgba(hex_color) 39 | 40 | 41 | def setup_camera(loc, rot): 42 | """ 43 | create and setup the camera 44 | """ 45 | bpy.ops.object.camera_add(location=loc, rotation=rot) 46 | camera = active_object() 47 | 48 | # set the camera as the "active camera" in the scene 49 | bpy.context.scene.camera = camera 50 | 51 | # set the Focal Length of the camera 52 | camera.data.lens = 70 53 | 54 | camera.data.passepartout_alpha = 0.9 55 | 56 | empty = track_empty(camera) 57 | 58 | return empty 59 | 60 | 61 | def set_scene_props(fps, loop_seconds): 62 | """ 63 | Set scene properties 64 | """ 65 | frame_count = fps * loop_seconds 66 | 67 | scene = bpy.context.scene 68 | scene.frame_end = frame_count 69 | 70 | # set the world background to black 71 | world = bpy.data.worlds["World"] 72 | if "Background" in world.node_tree.nodes: 73 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 74 | 75 | scene.render.fps = fps 76 | 77 | scene.frame_current = 1 78 | scene.frame_start = 1 79 | 80 | scene.render.engine = "CYCLES" 81 | 82 | # Use the GPU to render 83 | # scene.cycles.device = 'GPU' 84 | 85 | # Use the CPU to render 86 | scene.cycles.device = "CPU" 87 | 88 | scene.cycles.samples = 1024 89 | 90 | scene.view_settings.look = "Very High Contrast" 91 | 92 | set_1080px_square_render_res() 93 | 94 | 95 | def scene_setup(i=0): 96 | fps = 30 97 | loop_seconds = 12 98 | frame_count = fps * loop_seconds 99 | 100 | project_name = "in_or_out" 101 | bpy.context.scene.render.image_settings.file_format = "FFMPEG" 102 | bpy.context.scene.render.ffmpeg.format = "MPEG4" 103 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/loop_{i}.mp4" 104 | 105 | seed = 0 106 | if seed: 107 | random.seed(seed) 108 | else: 109 | time_seed() 110 | 111 | # Utility Building Blocks 112 | use_clean_scene_experimental = False 113 | if use_clean_scene_experimental: 114 | clean_scene_experimental() 115 | else: 116 | clean_scene() 117 | 118 | set_scene_props(fps, loop_seconds) 119 | 120 | loc = (0, 0, 7) 121 | rot = (0, 0, 0) 122 | setup_camera(loc, rot) 123 | 124 | context = { 125 | "frame_count": frame_count, 126 | } 127 | 128 | return context 129 | 130 | 131 | def add_light(): 132 | bpy.ops.object.light_add(type="AREA", radius=1, location=(0, 0, 2)) 133 | bpy.context.object.data.energy = 100 134 | bpy.context.object.data.color = get_random_color()[:3] 135 | bpy.context.object.data.shape = "DISK" 136 | 137 | 138 | def apply_glare_composite_effect(): 139 | bpy.context.scene.use_nodes = True 140 | 141 | render_layer_node = bpy.context.scene.node_tree.nodes.get("Render Layers") 142 | comp_node = bpy.context.scene.node_tree.nodes.get("Composite") 143 | 144 | # remove node_glare from the previous run 145 | old_node_glare = bpy.context.scene.node_tree.nodes.get("Glare") 146 | if old_node_glare: 147 | bpy.context.scene.node_tree.nodes.remove(old_node_glare) 148 | 149 | # create Glare node 150 | node_glare = bpy.context.scene.node_tree.nodes.new(type="CompositorNodeGlare") 151 | node_glare.size = 7 152 | node_glare.glare_type = "FOG_GLOW" 153 | node_glare.quality = "HIGH" 154 | node_glare.threshold = 0.2 155 | 156 | # create links 157 | bpy.context.scene.node_tree.links.new(render_layer_node.outputs["Image"], node_glare.inputs["Image"]) 158 | bpy.context.scene.node_tree.links.new(node_glare.outputs["Image"], comp_node.inputs["Image"]) 159 | 160 | 161 | ################################################################ 162 | # helper functions END 163 | ################################################################ 164 | 165 | 166 | def apply_metaball_material(): 167 | color = get_random_color() 168 | material = create_reflective_material(color, name="metaball", roughness=0.1, specular=0.5) 169 | 170 | primary_metaball = bpy.data.metaballs[0] 171 | primary_metaball.materials.append(material) 172 | 173 | 174 | def create_metaball_path(context): 175 | bpy.ops.curve.primitive_bezier_circle_add() 176 | path = active_object() 177 | 178 | path.data.path_duration = context["frame_count"] 179 | 180 | animate_360_rotation(Axis.X, context["frame_count"], path, clockwise=random.randint(0, 1)) 181 | 182 | apply_random_rotation() 183 | 184 | if random.randint(0, 1): 185 | path.scale.x *= random.uniform(0.1, 0.4) 186 | else: 187 | path.scale.y *= random.uniform(0.1, 0.4) 188 | 189 | return path 190 | 191 | 192 | def create_metaball(path): 193 | bpy.ops.object.metaball_add() 194 | ball = active_object() 195 | ball.data.render_resolution = 0.05 196 | ball.scale *= random.uniform(0.05, 0.5) 197 | 198 | bpy.ops.object.constraint_add(type="FOLLOW_PATH") 199 | bpy.context.object.constraints["Follow Path"].target = path 200 | bpy.ops.constraint.followpath_path_animate(constraint="Follow Path", owner="OBJECT") 201 | 202 | 203 | def create_centerpiece(context): 204 | metaball_count = 10 205 | 206 | for _ in range(metaball_count): 207 | path = create_metaball_path(context) 208 | create_metaball(path) 209 | 210 | apply_metaball_material() 211 | 212 | 213 | def create_background(): 214 | bpy.ops.curve.primitive_bezier_circle_add(radius=1.5) 215 | bpy.context.object.data.resolution_u = 64 216 | bpy.context.object.data.bevel_depth = 0.05 217 | 218 | color = get_random_color() 219 | apply_emission_material(color, energy=30) 220 | 221 | 222 | def main(): 223 | """ 224 | Python code to generate this animation 225 | https://www.artstation.com/artwork/KO66oG 226 | """ 227 | context = scene_setup() 228 | create_centerpiece(context) 229 | create_background() 230 | add_light() 231 | apply_glare_composite_effect() 232 | 233 | 234 | if __name__ == "__main__": 235 | main() 236 | -------------------------------------------------------------------------------- /outline/outline_done_bpybb.py: -------------------------------------------------------------------------------- 1 | import math 2 | import pathlib 3 | import random 4 | 5 | import bpy 6 | 7 | from bpybb.addon import enable_extra_curves 8 | from bpybb.animate import animate_360_rotation 9 | from bpybb.animate import create_data_animation_loop 10 | from bpybb.color import hex_color_to_rgba 11 | from bpybb.hdri import apply_hdri 12 | from bpybb.material import apply_material 13 | from bpybb.object import track_empty, add_empty 14 | from bpybb.output import set_1080px_square_render_res 15 | from bpybb.random import time_seed 16 | from bpybb.utils import clean_scene, active_object, clean_scene_experimental, parent, Axis 17 | 18 | ################################################################ 19 | # helper functions BEGIN 20 | ################################################################ 21 | 22 | 23 | def setup_camera(frame_count): 24 | """ 25 | create and setup the camera 26 | """ 27 | bpy.ops.object.camera_add() 28 | camera = active_object() 29 | 30 | start_location = (-0.5, 7, 0) 31 | camera.location = start_location 32 | mid_location = (-0.5, 8.5, 1.5) 33 | create_data_animation_loop(camera, "location", start_location, mid_location, start_frame=1, loop_length=frame_count, linear_extrapolation=False) 34 | 35 | # set the camera as the "active camera" in the scene 36 | bpy.context.scene.camera = camera 37 | 38 | # set the Focal Length of the camera 39 | camera.data.lens = 70 40 | camera.data.dof.use_dof = True 41 | camera.data.dof.aperture_fstop = 1.1 42 | 43 | camera.data.passepartout_alpha = 0.9 44 | 45 | empty = track_empty(camera) 46 | 47 | camera.data.dof.focus_object = empty 48 | 49 | return empty 50 | 51 | 52 | def set_scene_props(fps, loop_seconds): 53 | """ 54 | Set scene properties 55 | """ 56 | frame_count = fps * loop_seconds 57 | 58 | scene = bpy.context.scene 59 | scene.frame_end = frame_count 60 | 61 | # set the world background to black 62 | world = bpy.data.worlds["World"] 63 | if "Background" in world.node_tree.nodes: 64 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 65 | 66 | scene.render.fps = fps 67 | 68 | scene.frame_current = 1 69 | scene.frame_start = 1 70 | 71 | scene.render.engine = "CYCLES" 72 | 73 | # Use the GPU to render 74 | scene.cycles.device = "GPU" 75 | 76 | # Use the CPU to render 77 | # scene.cycles.device = "CPU" 78 | 79 | scene.cycles.samples = 300 80 | 81 | scene.view_settings.look = "Very High Contrast" 82 | 83 | set_1080px_square_render_res() 84 | 85 | 86 | def scene_setup(i=0): 87 | fps = 30 88 | loop_seconds = 12 89 | frame_count = fps * loop_seconds 90 | 91 | project_name = "outline" 92 | bpy.context.scene.render.image_settings.file_format = "FFMPEG" 93 | bpy.context.scene.render.ffmpeg.format = "MPEG4" 94 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/loop_{i}.mp4" 95 | 96 | seed = 0 97 | if seed: 98 | random.seed(seed) 99 | else: 100 | time_seed() 101 | 102 | # Utility Building Blocks 103 | use_clean_scene_experimental = False 104 | if use_clean_scene_experimental: 105 | clean_scene_experimental() 106 | else: 107 | clean_scene() 108 | 109 | set_scene_props(fps, loop_seconds) 110 | 111 | loop_frame_count = frame_count + 1 112 | setup_camera(loop_frame_count) 113 | 114 | context = {"frame_count": frame_count, "loop_frame_count": loop_frame_count} 115 | 116 | return context 117 | 118 | 119 | def add_lights(): 120 | """ 121 | I used this HDRI: https://polyhaven.com/a/studio_small_03 122 | 123 | Please consider supporting polyhaven.com @ https://www.patreon.com/polyhaven/overview 124 | """ 125 | # update the path to where you downloaded the HDRI 126 | path_to_image = str(pathlib.Path.home() / "tmp" / "studio_small_03_1k.exr") 127 | 128 | color = get_random_color() 129 | apply_hdri(path_to_image, bg_color=color, hdri_light_strength=1, bg_strength=1) 130 | 131 | 132 | def render_loop(): 133 | bpy.ops.render.render(animation=True) 134 | 135 | 136 | def create_metallic_material(color, name=None, roughness=0.1, return_nodes=False): 137 | if name is None: 138 | name = "" 139 | 140 | material = bpy.data.materials.new(name=f"material.metallic.{name}") 141 | material.use_nodes = True 142 | 143 | material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = color 144 | material.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = roughness 145 | material.node_tree.nodes["Principled BSDF"].inputs["Metallic"].default_value = 1.0 146 | 147 | if return_nodes: 148 | return material, material.node_tree.nodes 149 | else: 150 | return material 151 | 152 | 153 | def get_random_color(): 154 | hex_color = random.choice( 155 | [ 156 | "#FC766A", 157 | "#5B84B1", 158 | "#5F4B8B", 159 | "#E69A8D", 160 | "#42EADD", 161 | "#CDB599", 162 | "#00A4CC", 163 | "#F95700", 164 | "#00203F", 165 | "#ADEFD1", 166 | "#606060", 167 | "#D6ED17", 168 | "#ED2B33", 169 | "#D85A7F", 170 | ] 171 | ) 172 | 173 | return hex_color_to_rgba(hex_color) 174 | 175 | 176 | ################################################################ 177 | # helper functions END 178 | ################################################################ 179 | 180 | 181 | def create_profile_obj(): 182 | bpy.ops.curve.primitive_bezier_circle_add(radius=0.02, enter_editmode=False) 183 | return active_object() 184 | 185 | 186 | def add_curve(loop_frame_count, material, profile_obj): 187 | 188 | curve_ctrl = add_empty() 189 | curve_ctrl.rotation_euler.x = math.radians(90) 190 | curve_ctrl.rotation_euler.z = math.radians(45) 191 | 192 | bpy.ops.curve.simple(Simple_Type="Arc", Simple_endangle=120, edit_mode=False, use_cyclic_u=False) 193 | curve = active_object() 194 | 195 | apply_material(material) 196 | 197 | curve.location.y = -0.25 198 | 199 | curve.data.bevel_mode = "OBJECT" 200 | curve.data.bevel_object = profile_obj 201 | curve.data.use_fill_caps = True 202 | curve.data.resolution_u = 32 203 | 204 | animate_360_rotation(Axis.Z, loop_frame_count, obj=curve, clockwise=False, linear=True, start_frame=1) 205 | 206 | parent(curve, curve_ctrl) 207 | 208 | return curve_ctrl 209 | 210 | 211 | def create_centerpiece(context): 212 | 213 | count = 32 214 | rotation_step = 360 / count 215 | 216 | current_rotation = 0 217 | 218 | profile_obj = create_profile_obj() 219 | 220 | material = create_metallic_material(get_random_color(), name="material", roughness=0.1) 221 | 222 | for _ in range(count): 223 | rotation_ctrl = add_empty() 224 | rotation_ctrl.rotation_euler.y = math.radians(current_rotation) 225 | 226 | curve_ctrl = add_curve(context["loop_frame_count"], material, profile_obj) 227 | parent(curve_ctrl, rotation_ctrl) 228 | 229 | current_rotation += rotation_step 230 | 231 | 232 | def main(): 233 | """ 234 | Python code to generate this animation 235 | https://www.artstation.com/artwork/klEGRy 236 | """ 237 | enable_extra_curves() 238 | 239 | context = scene_setup() 240 | create_centerpiece(context) 241 | add_lights() 242 | 243 | 244 | if __name__ == "__main__": 245 | main() 246 | -------------------------------------------------------------------------------- /sequence_editor_frame_import/sequence_editor_frame_import_done.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import os 3 | import pprint 4 | 5 | import bpy 6 | 7 | 8 | def clean_sequencer(sequence_editor_area): 9 | with bpy.context.temp_override(area=sequence_editor_area): 10 | bpy.ops.sequencer.select_all(action="SELECT") 11 | bpy.ops.sequencer.delete() 12 | 13 | 14 | def find_sequence_editor(): 15 | for area in bpy.context.window.screen.areas: 16 | if area.type == "SEQUENCE_EDITOR": 17 | return area 18 | return None 19 | 20 | 21 | def get_image_files(image_folder_path, image_extention=".png"): 22 | image_files = list() 23 | for file_name in os.listdir(image_folder_path): 24 | if file_name.endswith(image_extention): 25 | image_files.append(file_name) 26 | image_files.sort() 27 | 28 | pprint.pprint(image_files) 29 | 30 | return image_files 31 | 32 | 33 | def get_image_dimensions(image_path): 34 | image = bpy.data.images.load(image_path) 35 | width, height = image.size 36 | return width, height 37 | 38 | 39 | def set_up_output_params(image_folder_path, image_files, fps): 40 | frame_count = len(image_files) 41 | 42 | scene = bpy.context.scene 43 | 44 | scene.frame_end = frame_count 45 | 46 | image_path = os.path.join(image_folder_path, image_files[0]) 47 | 48 | width, height = get_image_dimensions(image_path) 49 | 50 | scene.render.resolution_y = height 51 | scene.render.resolution_x = width 52 | 53 | scene.render.fps = fps 54 | scene.render.image_settings.file_format = "FFMPEG" 55 | scene.render.ffmpeg.format = "MPEG4" 56 | scene.render.ffmpeg.constant_rate_factor = "PERC_LOSSLESS" 57 | 58 | now = datetime.now() 59 | time = now.strftime("%H-%M-%S") 60 | filepath = os.path.join(image_folder_path, f"anim_{time}.mp4") 61 | scene.render.filepath = filepath 62 | 63 | 64 | def gen_video_from_images(image_folder_path, fps): 65 | 66 | image_files = get_image_files(image_folder_path) 67 | 68 | set_up_output_params(image_folder_path, image_files, fps) 69 | 70 | sequence_editor_area = find_sequence_editor() 71 | 72 | clean_sequencer(sequence_editor_area) 73 | 74 | file_info = list() 75 | for image_name in image_files: 76 | file_info.append({"name": image_name}) 77 | 78 | with bpy.context.temp_override(area=sequence_editor_area): 79 | bpy.ops.sequencer.image_strip_add( 80 | directory=image_folder_path + os.sep, 81 | files=file_info, 82 | frame_start=1, 83 | ) 84 | 85 | bpy.ops.render.render(animation=True) 86 | 87 | 88 | def main(): 89 | """ 90 | Python code to generate a mp4 video for a folder with png(s) 91 | """ 92 | image_folder_path = "C:\\tmp\\day334_3" 93 | 94 | # uncomment the next two lines when running on macOS or Linux 95 | # user_folder = os.path.expanduser("~") 96 | # image_folder_path = f"{user_folder}/tmp/my_rendered_frames" 97 | 98 | fps = 30 99 | gen_video_from_images(image_folder_path, fps) 100 | 101 | 102 | if __name__ == "__main__": 103 | main() 104 | -------------------------------------------------------------------------------- /sequence_editor_frame_import/sequence_editor_frame_import_start.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import os 3 | import pprint 4 | 5 | import bpy 6 | 7 | 8 | def gen_video_from_images(image_folder_path, fps): 9 | pass 10 | 11 | 12 | def main(): 13 | """ 14 | Python code to generate a mp4 video for a folder with png(s) 15 | """ 16 | image_folder_path = "C:\\tmp\\day334_3" 17 | 18 | # uncomment the next two lines when running on macOS or Linux 19 | # user_folder = os.path.expanduser("~") 20 | # image_folder_path = f"{user_folder}/tmp/my_rendered_frames" 21 | 22 | fps = 30 23 | gen_video_from_images(image_folder_path, fps) 24 | 25 | 26 | if __name__ == "__main__": 27 | main() 28 | -------------------------------------------------------------------------------- /sequence_editor_stitched_together_clips/sequence_editor_stitched_together_clips_done.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import shutil 4 | 5 | import bpy 6 | 7 | 8 | def clean_sequencer(sequence_context): 9 | bpy.ops.sequencer.select_all(sequence_context, action="SELECT") 10 | bpy.ops.sequencer.delete(sequence_context) 11 | 12 | 13 | def find_sequence_editor(): 14 | for area in bpy.context.window.screen.areas: 15 | if area.type == "SEQUENCE_EDITOR": 16 | return area 17 | return None 18 | 19 | 20 | def set_up_output_params(folder_path): 21 | scene = bpy.context.scene 22 | 23 | scene.render.image_settings.file_format = "FFMPEG" 24 | scene.render.ffmpeg.format = "MPEG4" 25 | scene.render.ffmpeg.constant_rate_factor = "PERC_LOSSLESS" 26 | 27 | now = datetime.datetime.now() 28 | time = now.strftime("%H-%M-%S") 29 | filepath = os.path.join(folder_path, f"stitched_together_{time}.mp4") 30 | scene.render.filepath = filepath 31 | 32 | 33 | def clean_proxies(video_folder_path): 34 | """ 35 | This will delete the BL_proxies folder 36 | """ 37 | 38 | def on_error(function, path, excinfo): 39 | print(f"Failed to remove {path}\n{excinfo}") 40 | 41 | bl_proxy_path = os.path.join(video_folder_path, "BL_proxy") 42 | if os.path.exists(bl_proxy_path): 43 | print(f"Removing the BL_proxies folder in {bl_proxy_path}") 44 | shutil.rmtree(bl_proxy_path, ignore_errors=False, onerror=on_error) 45 | 46 | 47 | def trim_the_video(clip_start_offset_frame, clip_frame_count): 48 | # trim the start of the clip 49 | bpy.context.active_sequence_strip.frame_offset_start = clip_start_offset_frame 50 | 51 | # trim the end of the clip 52 | bpy.context.active_sequence_strip.frame_final_duration = clip_frame_count 53 | 54 | 55 | def move_the_clip_into_position(start_frame_pos, clip_start_offset_frame): 56 | bpy.context.active_sequence_strip.frame_start = start_frame_pos - clip_start_offset_frame 57 | 58 | 59 | def apply_fade_in_to_clip(clip_transition_overlap): 60 | # make sure the clips overlap 61 | bpy.context.active_sequence_strip.frame_start -= clip_transition_overlap 62 | 63 | bpy.ops.sequencer.fades_add(type="IN") 64 | 65 | 66 | def create_transition_between_videos(video_folder_path, fps, clip_length_sec, video_names, clip_middle_offset): 67 | 68 | sequence_editor = find_sequence_editor() 69 | 70 | sequence_editor_context = { 71 | "area": sequence_editor, 72 | } 73 | clean_sequencer(sequence_editor_context) 74 | clean_proxies(video_folder_path) 75 | 76 | clip_frame_count = clip_length_sec * fps 77 | 78 | start_frame_pos = 0 79 | clip_transition_overlap = 1 * fps 80 | for file_name in os.listdir(video_folder_path): 81 | if file_name not in video_names: 82 | continue 83 | 84 | video_name = file_name 85 | 86 | # create a full path to the video 87 | video_path = os.path.join(video_folder_path, video_name) 88 | print(f"Processing video {video_path}") 89 | 90 | # add video to the sequence editor 91 | bpy.ops.sequencer.movie_strip_add( 92 | sequence_editor_context, 93 | filepath=video_path, 94 | directory=video_folder_path + os.sep, 95 | sound=False, 96 | ) 97 | 98 | # get the middle of the clip 99 | mid_frame = int(bpy.context.active_sequence_strip.frame_final_duration / 2) 100 | 101 | # apply custom offset 102 | if clip_middle_offset.get(video_name): 103 | mid_frame += clip_middle_offset.get(video_name) 104 | 105 | clip_start_offset_frame = mid_frame - clip_frame_count 106 | 107 | trim_the_video(clip_start_offset_frame, clip_frame_count) 108 | 109 | move_the_clip_into_position(start_frame_pos, clip_start_offset_frame) 110 | 111 | # if this is not the first clip in the sequence, add a "fade in" transition 112 | if start_frame_pos != 0: 113 | apply_fade_in_to_clip(clip_transition_overlap) 114 | 115 | # update the starting position for the next clip 116 | start_frame_pos = bpy.context.active_sequence_strip.frame_final_end 117 | 118 | # Set the final frame 119 | bpy.context.scene.frame_end = bpy.context.active_sequence_strip.frame_final_end 120 | 121 | # Render the clip sequence 122 | # bpy.ops.render.render(animation=True) 123 | 124 | 125 | def main(): 126 | """ 127 | Python code to create short clips from videos and stich them back to back 128 | with a transition 129 | """ 130 | 131 | video_folder_path = r"C:\tmp\my_videos" 132 | 133 | # uncomment the next two lines when running on macOS or Linux 134 | # user_folder = os.path.expanduser("~") 135 | # video_folder_path = f"{user_folder}/tmp/my_videos" 136 | 137 | set_up_output_params(video_folder_path) 138 | 139 | video_names = [ 140 | "video_01.mp4", 141 | "video_02.mp4", 142 | "video_03.mp4", 143 | "video_04.mp4", 144 | ] 145 | 146 | fps = 30 147 | clip_middle_offset = { 148 | "video_01.mp4": 8 * fps, 149 | "video_03.mp4": 4 * fps, 150 | } 151 | 152 | # The length of the clips in seconds 153 | clip_length_sec = 4 154 | 155 | create_transition_between_videos(video_folder_path, fps, clip_length_sec, video_names, clip_middle_offset) 156 | 157 | 158 | if __name__ == "__main__": 159 | main() 160 | -------------------------------------------------------------------------------- /sequence_editor_stitched_together_clips/sequence_editor_stitched_together_clips_start.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import shutil 4 | 5 | import bpy 6 | 7 | 8 | def clean_sequencer(sequence_context): 9 | bpy.ops.sequencer.select_all(sequence_context, action="SELECT") 10 | bpy.ops.sequencer.delete(sequence_context) 11 | 12 | 13 | def find_sequence_editor(): 14 | for area in bpy.context.window.screen.areas: 15 | if area.type == "SEQUENCE_EDITOR": 16 | return area 17 | return None 18 | 19 | 20 | def set_up_output_params(folder_path): 21 | scene = bpy.context.scene 22 | 23 | scene.render.image_settings.file_format = "FFMPEG" 24 | scene.render.ffmpeg.format = "MPEG4" 25 | scene.render.ffmpeg.constant_rate_factor = "PERC_LOSSLESS" 26 | 27 | now = datetime.datetime.now() 28 | time = now.strftime("%H-%M-%S") 29 | filepath = os.path.join(folder_path, f"stitched_together_{time}.mp4") 30 | scene.render.filepath = filepath 31 | 32 | 33 | def clean_proxies(video_folder_path): 34 | """ 35 | This will delete the BL_proxies folder 36 | """ 37 | 38 | def on_error(function, path, excinfo): 39 | print(f"Failed to remove {path}\n{excinfo}") 40 | 41 | bl_proxy_path = os.path.join(video_folder_path, "BL_proxy") 42 | if os.path.exists(bl_proxy_path): 43 | print(f"Removing the BL_proxies folder in {bl_proxy_path}") 44 | shutil.rmtree(bl_proxy_path, ignore_errors=False, onerror=on_error) 45 | 46 | 47 | def create_transition_between_videos(video_folder_path): 48 | 49 | sequence_editor = find_sequence_editor() 50 | 51 | sequence_editor_context = { 52 | "area": sequence_editor, 53 | } 54 | clean_sequencer(sequence_editor_context) 55 | clean_proxies(video_folder_path) 56 | 57 | # TODO: add code here 58 | 59 | # Render the clip sequence 60 | # bpy.ops.render.render(animation=True) 61 | 62 | 63 | def main(): 64 | """ 65 | Python code to create short clips from videos and stich them back to back 66 | with a transition 67 | """ 68 | 69 | video_folder_path = r"C:\tmp\my_videos" 70 | 71 | # uncomment the next two lines when running on macOS or Linux 72 | # user_folder = os.path.expanduser("~") 73 | # video_folder_path = f"{user_folder}/tmp/my_videos" 74 | 75 | set_up_output_params(video_folder_path) 76 | 77 | video_names = [ 78 | "video_01.mp4", 79 | "video_02.mp4", 80 | "video_03.mp4", 81 | "video_04.mp4", 82 | ] 83 | 84 | fps = 30 85 | clip_middle_offset = { 86 | "video_01.mp4": 8 * fps, 87 | "video_03.mp4": 4 * fps, 88 | } 89 | 90 | # The length of the clips in seconds 91 | clip_length_sec = 4 92 | 93 | create_transition_between_videos(video_folder_path) 94 | 95 | 96 | if __name__ == "__main__": 97 | main() 98 | -------------------------------------------------------------------------------- /stack_overflow/stack_overflow_done_bpybb.py: -------------------------------------------------------------------------------- 1 | """ 2 | See YouTube tutorial here: https://youtu.be/56hht5bMy3A 3 | """ 4 | import random 5 | import math 6 | 7 | import bpy 8 | 9 | from bpybb.color import hex_color_to_rgba 10 | from bpybb.object import track_empty, add_empty 11 | from bpybb.output import set_1080px_square_render_res 12 | from bpybb.random import time_seed 13 | from bpybb.addon import enable_extra_meshes, enable_mod_tools 14 | from bpybb.animate import set_fcurve_interpolation_to_linear 15 | from bpybb.utils import clean_scene, active_object, clean_scene_experimental, duplicate_object 16 | 17 | 18 | ################################################################ 19 | # helper functions BEGIN 20 | ################################################################ 21 | 22 | 23 | def get_random_color(): 24 | hex_color = random.choice( 25 | [ 26 | "#402217", 27 | "#515559", 28 | "#727273", 29 | "#8C593B", 30 | "#A64E1B", 31 | "#A65D05", 32 | "#A68A80", 33 | "#A6A6A6", 34 | "#BF6415", 35 | "#BF8B2A", 36 | "#C5992E", 37 | "#E8BB48", 38 | "#F2DC6B", 39 | ] 40 | ) 41 | 42 | return hex_color_to_rgba(hex_color) 43 | 44 | 45 | def setup_camera(loc, rot): 46 | """ 47 | create and setup the camera 48 | """ 49 | bpy.ops.object.camera_add(location=loc, rotation=rot) 50 | camera = active_object() 51 | 52 | # set the camera as the "active camera" in the scene 53 | bpy.context.scene.camera = camera 54 | 55 | # set the Focal Length of the camera 56 | camera.data.lens = 65 57 | 58 | camera.data.passepartout_alpha = 0.9 59 | 60 | empty = track_empty(camera) 61 | 62 | return empty 63 | 64 | 65 | def set_scene_props(fps, loop_seconds): 66 | """ 67 | Set scene properties 68 | """ 69 | frame_count = fps * loop_seconds 70 | 71 | scene = bpy.context.scene 72 | scene.frame_end = frame_count 73 | 74 | # set the world background to black 75 | world = bpy.data.worlds["World"] 76 | if "Background" in world.node_tree.nodes: 77 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 78 | 79 | scene.render.fps = fps 80 | 81 | scene.frame_current = 1 82 | scene.frame_start = 1 83 | 84 | scene.render.engine = "CYCLES" 85 | 86 | # Use the GPU to render 87 | scene.cycles.device = "GPU" 88 | 89 | # Use the CPU to render 90 | # scene.cycles.device = "CPU" 91 | 92 | scene.cycles.samples = 300 93 | 94 | scene.view_settings.look = "Very High Contrast" 95 | 96 | set_1080px_square_render_res() 97 | 98 | 99 | def scene_setup(i=0): 100 | fps = 30 101 | loop_seconds = 12 102 | frame_count = fps * loop_seconds 103 | 104 | project_name = "stack_overflow" 105 | bpy.context.scene.render.image_settings.file_format = "FFMPEG" 106 | bpy.context.scene.render.ffmpeg.format = "MPEG4" 107 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/loop_{i}.mp4" 108 | 109 | seed = 0 110 | if seed: 111 | random.seed(seed) 112 | else: 113 | time_seed() 114 | 115 | # Utility Building Blocks 116 | use_clean_scene_experimental = False 117 | if use_clean_scene_experimental: 118 | clean_scene_experimental() 119 | else: 120 | clean_scene() 121 | 122 | set_scene_props(fps, loop_seconds) 123 | 124 | z_coord = 1 125 | loc = (6.5, -3, z_coord) 126 | rot = (0, 0, 0) 127 | empty = setup_camera(loc, rot) 128 | empty.location.z = 2 129 | 130 | context = { 131 | "frame_count": frame_count, 132 | "frame_count_loop": frame_count + 1, 133 | } 134 | 135 | return context 136 | 137 | 138 | def create_metallic_material(color, name=None, roughness=0.1, return_nodes=False): 139 | if name is None: 140 | name = "" 141 | 142 | material = bpy.data.materials.new(name=f"material.metallic.{name}") 143 | material.use_nodes = True 144 | 145 | material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = color 146 | material.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = roughness 147 | material.node_tree.nodes["Principled BSDF"].inputs["Metallic"].default_value = 1.0 148 | 149 | if return_nodes: 150 | return material, material.node_tree.nodes 151 | else: 152 | return material 153 | 154 | 155 | def apply_metallic_material(color, name=None, roughness=0.1): 156 | material = create_metallic_material(color, name=name, roughness=roughness) 157 | 158 | obj = active_object() 159 | obj.data.materials.append(material) 160 | 161 | 162 | def add_lights(): 163 | rig_obj, empty = create_light_rig(light_count=3, light_type="AREA", rig_radius=5.0, energy=150) 164 | rig_obj.location.z = 3 165 | 166 | bpy.ops.object.light_add(type="AREA", radius=5, location=(0, 0, 5)) 167 | 168 | 169 | def create_light_rig(light_count, light_type="AREA", rig_radius=2.0, light_radius=1.0, energy=100): 170 | bpy.ops.mesh.primitive_circle_add(vertices=light_count, radius=rig_radius) 171 | rig_obj = active_object() 172 | 173 | empty = add_empty(name=f"empty.tracker-target.lights") 174 | 175 | for i in range(light_count): 176 | loc = rig_obj.data.vertices[i].co 177 | 178 | bpy.ops.object.light_add(type=light_type, radius=light_radius, location=loc) 179 | light = active_object() 180 | light.data.energy = energy 181 | light.parent = rig_obj 182 | 183 | bpy.ops.object.constraint_add(type="TRACK_TO") 184 | light.constraints["Track To"].target = empty 185 | 186 | return rig_obj, empty 187 | 188 | 189 | ################################################################ 190 | # helper functions END 191 | ################################################################ 192 | 193 | 194 | def make_surface(color): 195 | # this operator is found in the "Add Mesh Extra Objects" add-on 196 | bpy.ops.mesh.primitive_z_function_surface(div_x=64, div_y=64, size_x=1, size_y=1) 197 | 198 | surface = active_object() 199 | 200 | bpy.ops.object.shade_smooth() 201 | surface.data.use_auto_smooth = True 202 | 203 | bpy.ops.object.modifier_add(type="SOLIDIFY") 204 | 205 | bpy.ops.object.modifier_add(type="BEVEL") 206 | surface.modifiers["Bevel"].width = 0.001 207 | surface.modifiers["Bevel"].limit_method = "NONE" 208 | 209 | # this operator is found in the "Modifier Tools" add-on 210 | bpy.ops.object.apply_all_modifiers() 211 | 212 | apply_metallic_material(color, name="metallic", roughness=random.uniform(0.35, 0.65)) 213 | 214 | return surface 215 | 216 | 217 | def update_object(obj): 218 | obj.scale *= 1.5 219 | obj.location.z = 2 220 | obj.rotation_euler.z = math.radians(270) 221 | 222 | 223 | def animate_object_update(context, obj, current_frame): 224 | obj.keyframe_insert("scale", frame=current_frame) 225 | obj.keyframe_insert("location", frame=current_frame) 226 | obj.keyframe_insert("rotation_euler", frame=current_frame) 227 | 228 | update_object(obj) 229 | 230 | frame = current_frame + context["frame_count_loop"] 231 | 232 | obj.keyframe_insert("scale", frame=frame) 233 | obj.keyframe_insert("location", frame=frame) 234 | obj.keyframe_insert("rotation_euler", frame=frame) 235 | 236 | set_fcurve_interpolation_to_linear() 237 | 238 | 239 | def create_centerpiece(context, color): 240 | 241 | frame_step = 6 242 | buffer = 1 243 | count = int((context["frame_count_loop"] * 2) / frame_step) + buffer 244 | current_frame = -context["frame_count_loop"] 245 | 246 | surface = make_surface(color) 247 | 248 | for _ in range(count): 249 | 250 | duplicate_surface = duplicate_object(surface) 251 | 252 | animate_object_update(context, duplicate_surface, current_frame) 253 | 254 | current_frame += frame_step 255 | 256 | 257 | def create_background(color): 258 | bottom_surface = make_surface(color) 259 | bottom_surface.location.z -= 0.001 260 | 261 | top_surface = make_surface(color) 262 | update_object(top_surface) 263 | top_surface.location.z += 0.001 264 | 265 | bpy.ops.mesh.primitive_plane_add(size=100, location=(0, 0, 0.5)) 266 | 267 | 268 | def main(): 269 | """ 270 | Python code to generate this animation 271 | https://www.artstation.com/artwork/nEw0mK 272 | """ 273 | context = scene_setup() 274 | 275 | enable_extra_meshes() 276 | enable_mod_tools() 277 | 278 | color = get_random_color() 279 | create_centerpiece(context, color) 280 | create_background(color) 281 | add_lights() 282 | 283 | 284 | if __name__ == "__main__": 285 | main() 286 | -------------------------------------------------------------------------------- /video_grid/video_grid_done.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | 4 | import bpy 5 | import addon_utils 6 | 7 | ################################################################ 8 | # helper functions BEGIN 9 | ################################################################ 10 | 11 | 12 | def purge_orphans(): 13 | """ 14 | Remove all orphan data blocks 15 | 16 | see this from more info: 17 | https://youtu.be/3rNqVPtbhzc?t=149 18 | """ 19 | if bpy.app.version >= (3, 0, 0): 20 | # run this only for Blender versions 3.0 and higher 21 | bpy.ops.outliner.orphans_purge( 22 | do_local_ids=True, do_linked_ids=True, do_recursive=True 23 | ) 24 | else: 25 | # run this only for Blender versions lower than 3.0 26 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 27 | result = bpy.ops.outliner.orphans_purge() 28 | if result.pop() != "CANCELLED": 29 | purge_orphans() 30 | 31 | 32 | def clean_scene(): 33 | """ 34 | Removing all of the objects, collection, materials, particles, 35 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 36 | 37 | Checkout this video explanation with example 38 | 39 | "How to clean the scene with Python in Blender (with examples)" 40 | https://youtu.be/3rNqVPtbhzc 41 | """ 42 | # make sure the active object is not in Edit Mode 43 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 44 | bpy.ops.object.editmode_toggle() 45 | 46 | # make sure non of the objects are hidden from the viewport, selection, or disabled 47 | for obj in bpy.data.objects: 48 | obj.hide_set(False) 49 | obj.hide_select = False 50 | obj.hide_viewport = False 51 | 52 | # select all the object and delete them (just like pressing A + X + D in the viewport) 53 | bpy.ops.object.select_all(action="SELECT") 54 | bpy.ops.object.delete() 55 | 56 | # find all the collections and remove them 57 | collection_names = [col.name for col in bpy.data.collections] 58 | for name in collection_names: 59 | bpy.data.collections.remove(bpy.data.collections[name]) 60 | 61 | # in the case when you modify the world shader 62 | # delete and recreate the world object 63 | world_names = [world.name for world in bpy.data.worlds] 64 | for name in world_names: 65 | bpy.data.worlds.remove(bpy.data.worlds[name]) 66 | # create a new world data block 67 | bpy.ops.world.new() 68 | bpy.context.scene.world = bpy.data.worlds["World"] 69 | 70 | purge_orphans() 71 | 72 | 73 | def active_object(): 74 | """ 75 | returns the active object 76 | """ 77 | return bpy.context.active_object 78 | 79 | 80 | def time_seed(): 81 | """ 82 | Sets the random seed based on the time 83 | and copies the seed into the clipboard 84 | """ 85 | seed = time.time() 86 | print(f"seed: {seed}") 87 | random.seed(seed) 88 | 89 | # add the seed value to your clipboard 90 | bpy.context.window_manager.clipboard = str(seed) 91 | 92 | return seed 93 | 94 | 95 | def add_ctrl_empty(name=None): 96 | 97 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 98 | empty_ctrl = active_object() 99 | 100 | if name: 101 | empty_ctrl.name = name 102 | else: 103 | empty_ctrl.name = "empty.cntrl" 104 | 105 | return empty_ctrl 106 | 107 | 108 | def make_active(obj): 109 | bpy.ops.object.select_all(action="DESELECT") 110 | obj.select_set(True) 111 | bpy.context.view_layer.objects.active = obj 112 | 113 | 114 | def track_empty(obj): 115 | """ 116 | create an empty and add a 'Track To' constraint 117 | """ 118 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 119 | 120 | make_active(obj) 121 | bpy.ops.object.constraint_add(type="TRACK_TO") 122 | bpy.context.object.constraints["Track To"].target = empty 123 | 124 | return empty 125 | 126 | 127 | def setup_camera(loc, rot): 128 | """ 129 | create and setup the camera 130 | """ 131 | bpy.ops.object.camera_add(location=loc, rotation=rot) 132 | camera = active_object() 133 | 134 | # set the camera as the "active camera" in the scene 135 | bpy.context.scene.camera = camera 136 | 137 | # set the Focal Length of the camera 138 | camera.data.lens = 45 139 | 140 | camera.data.passepartout_alpha = 0.9 141 | 142 | empty = track_empty(camera) 143 | 144 | 145 | def set_1k_square_render_res(): 146 | """ 147 | Set the resolution of the rendered image to 1080 by 1080 148 | """ 149 | bpy.context.scene.render.resolution_x = 1080 150 | bpy.context.scene.render.resolution_y = 1080 151 | 152 | 153 | def set_scene_props(fps, loop_seconds): 154 | """ 155 | Set scene properties 156 | """ 157 | frame_count = fps * loop_seconds 158 | 159 | scene = bpy.context.scene 160 | scene.frame_end = frame_count 161 | 162 | # set the world background to black 163 | world = bpy.data.worlds["World"] 164 | if "Background" in world.node_tree.nodes: 165 | world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) 166 | 167 | scene.render.fps = fps 168 | 169 | scene.frame_current = 1 170 | scene.frame_start = 1 171 | 172 | scene.eevee.use_bloom = True 173 | scene.eevee.bloom_intensity = 0.005 174 | 175 | # set Ambient Occlusion properties 176 | scene.eevee.use_gtao = True 177 | scene.eevee.gtao_distance = 4 178 | scene.eevee.gtao_factor = 5 179 | 180 | scene.eevee.taa_render_samples = 64 181 | 182 | scene.view_settings.look = "Very High Contrast" 183 | 184 | set_1k_square_render_res() 185 | 186 | 187 | def setup_scene(): 188 | fps = 30 189 | loop_seconds = 12 190 | frame_count = fps * loop_seconds 191 | 192 | project_name = "loop_grid" 193 | bpy.context.scene.render.image_settings.file_format = "PNG" 194 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/" 195 | 196 | seed = 0 197 | if seed: 198 | random.seed(seed) 199 | else: 200 | time_seed() 201 | 202 | # Utility Building Blocks 203 | clean_scene() 204 | set_scene_props(fps, loop_seconds) 205 | 206 | loc = (0, 0, 5) 207 | rot = (0, 0, 0) 208 | setup_camera(loc, rot) 209 | 210 | 211 | def enable_import_images_as_planes(): 212 | 213 | loaded_default, loaded_state = addon_utils.check("io_import_images_as_planes") 214 | if not loaded_state: 215 | addon_utils.enable("io_import_images_as_planes") 216 | 217 | 218 | def add_light(): 219 | bpy.ops.object.light_add(type="SUN") 220 | sun = active_object() 221 | sun.data.energy = 2 222 | sun.data.specular_factor = 0 223 | sun.data.use_shadow = False 224 | 225 | 226 | ################################################################ 227 | # helper functions END 228 | ################################################################ 229 | 230 | 231 | def get_list_of_loops(): 232 | return [ 233 | "c:\\tmp\\project_cube_loops\\loop_0.mp4", 234 | "c:\\tmp\\project_cube_loops\\loop_1.mp4", 235 | "c:\\tmp\\project_cube_loops\\loop_10.mp4", 236 | "c:\\tmp\\project_cube_loops\\loop_11.mp4", 237 | "c:\\tmp\\project_cube_loops\\loop_12.mp4", 238 | "c:\\tmp\\project_cube_loops\\loop_13.mp4", 239 | "c:\\tmp\\project_cube_loops\\loop_14.mp4", 240 | "c:\\tmp\\project_cube_loops\\loop_15.mp4", 241 | "c:\\tmp\\project_cube_loops\\loop_2.mp4", 242 | "c:\\tmp\\project_cube_loops\\loop_3.mp4", 243 | "c:\\tmp\\project_cube_loops\\loop_4.mp4", 244 | "c:\\tmp\\project_cube_loops\\loop_5.mp4", 245 | "c:\\tmp\\project_cube_loops\\loop_6.mp4", 246 | "c:\\tmp\\project_cube_loops\\loop_7.mp4", 247 | "c:\\tmp\\project_cube_loops\\loop_8.mp4", 248 | "c:\\tmp\\project_cube_loops\\loop_9.mp4", 249 | ] 250 | 251 | 252 | def get_grid_step(path): 253 | bpy.ops.import_image.to_plane(files=[{"name": path}]) 254 | plane = active_object() 255 | x_step = plane.dimensions.x 256 | y_step = plane.dimensions.y 257 | 258 | bpy.ops.object.delete() 259 | 260 | return x_step, y_step 261 | 262 | 263 | def gen_centerpiece(): 264 | list_of_video_paths = get_list_of_loops() 265 | 266 | random.shuffle(list_of_video_paths) 267 | 268 | x_step, y_step = get_grid_step(list_of_video_paths[0]) 269 | 270 | start_x = -1.5 271 | stop_x = 2 272 | current_x = start_x 273 | 274 | current_y = -1.5 275 | 276 | for mp4_path in list_of_video_paths: 277 | bpy.ops.import_image.to_plane(files=[{"name": mp4_path}]) 278 | obj = active_object() 279 | obj.location.x = current_x 280 | obj.location.y = current_y 281 | 282 | shader_nodes = obj.active_material.node_tree.nodes 283 | shader_nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.0 284 | shader_nodes["Image Texture"].image_user.use_cyclic = True 285 | 286 | current_x += x_step 287 | 288 | if current_x > stop_x: 289 | current_x = start_x 290 | current_y += y_step 291 | 292 | 293 | def main(): 294 | """ 295 | Python code to generate a grid of loop animations from mp4 files 296 | 297 | Tutorial: https://www.youtube.com/watch?v=1toJeVNOdnk 298 | """ 299 | setup_scene() 300 | enable_import_images_as_planes() 301 | gen_centerpiece() 302 | add_light() 303 | 304 | 305 | if __name__ == "__main__": 306 | main() 307 | -------------------------------------------------------------------------------- /video_grid/video_grid_start.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | 4 | import bpy 5 | import addon_utils 6 | 7 | ################################################################ 8 | # helper functions BEGIN 9 | ################################################################ 10 | 11 | 12 | def purge_orphans(): 13 | """ 14 | Remove all orphan data blocks 15 | 16 | see this from more info: 17 | https://youtu.be/3rNqVPtbhzc?t=149 18 | """ 19 | if bpy.app.version >= (3, 0, 0): 20 | # run this only for Blender versions 3.0 and higher 21 | bpy.ops.outliner.orphans_purge( 22 | do_local_ids=True, do_linked_ids=True, do_recursive=True 23 | ) 24 | else: 25 | # run this only for Blender versions lower than 3.0 26 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 27 | result = bpy.ops.outliner.orphans_purge() 28 | if result.pop() != "CANCELLED": 29 | purge_orphans() 30 | 31 | 32 | def clean_scene(): 33 | """ 34 | Removing all of the objects, collection, materials, particles, 35 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 36 | 37 | Checkout this video explanation with example 38 | 39 | "How to clean the scene with Python in Blender (with examples)" 40 | https://youtu.be/3rNqVPtbhzc 41 | """ 42 | # make sure the active object is not in Edit Mode 43 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 44 | bpy.ops.object.editmode_toggle() 45 | 46 | # make sure non of the objects are hidden from the viewport, selection, or disabled 47 | for obj in bpy.data.objects: 48 | obj.hide_set(False) 49 | obj.hide_select = False 50 | obj.hide_viewport = False 51 | 52 | # select all the object and delete them (just like pressing A + X + D in the viewport) 53 | bpy.ops.object.select_all(action="SELECT") 54 | bpy.ops.object.delete() 55 | 56 | # find all the collections and remove them 57 | collection_names = [col.name for col in bpy.data.collections] 58 | for name in collection_names: 59 | bpy.data.collections.remove(bpy.data.collections[name]) 60 | 61 | # in the case when you modify the world shader 62 | # delete and recreate the world object 63 | world_names = [world.name for world in bpy.data.worlds] 64 | for name in world_names: 65 | bpy.data.worlds.remove(bpy.data.worlds[name]) 66 | # create a new world data block 67 | bpy.ops.world.new() 68 | bpy.context.scene.world = bpy.data.worlds["World"] 69 | 70 | purge_orphans() 71 | 72 | 73 | def active_object(): 74 | """ 75 | returns the active object 76 | """ 77 | return bpy.context.active_object 78 | 79 | 80 | def time_seed(): 81 | """ 82 | Sets the random seed based on the time 83 | and copies the seed into the clipboard 84 | """ 85 | seed = time.time() 86 | print(f"seed: {seed}") 87 | random.seed(seed) 88 | 89 | # add the seed value to your clipboard 90 | bpy.context.window_manager.clipboard = str(seed) 91 | 92 | return seed 93 | 94 | 95 | def add_ctrl_empty(name=None): 96 | 97 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 98 | empty_ctrl = active_object() 99 | 100 | if name: 101 | empty_ctrl.name = name 102 | else: 103 | empty_ctrl.name = "empty.cntrl" 104 | 105 | return empty_ctrl 106 | 107 | 108 | def make_active(obj): 109 | bpy.ops.object.select_all(action="DESELECT") 110 | obj.select_set(True) 111 | bpy.context.view_layer.objects.active = obj 112 | 113 | 114 | def track_empty(obj): 115 | """ 116 | create an empty and add a 'Track To' constraint 117 | """ 118 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 119 | 120 | make_active(obj) 121 | bpy.ops.object.constraint_add(type="TRACK_TO") 122 | bpy.context.object.constraints["Track To"].target = empty 123 | 124 | return empty 125 | 126 | 127 | def setup_camera(loc, rot): 128 | """ 129 | create and setup the camera 130 | """ 131 | bpy.ops.object.camera_add(location=loc, rotation=rot) 132 | camera = active_object() 133 | 134 | # set the camera as the "active camera" in the scene 135 | bpy.context.scene.camera = camera 136 | 137 | # set the Focal Length of the camera 138 | camera.data.lens = 45 139 | 140 | camera.data.passepartout_alpha = 0.9 141 | 142 | empty = track_empty(camera) 143 | 144 | 145 | def set_1k_square_render_res(): 146 | """ 147 | Set the resolution of the rendered image to 1080 by 1080 148 | """ 149 | bpy.context.scene.render.resolution_x = 1080 150 | bpy.context.scene.render.resolution_y = 1080 151 | 152 | 153 | def set_scene_props(fps, loop_seconds): 154 | """ 155 | Set scene properties 156 | """ 157 | frame_count = fps * loop_seconds 158 | 159 | scene = bpy.context.scene 160 | scene.frame_end = frame_count 161 | 162 | # set the world background to black 163 | world = bpy.data.worlds["World"] 164 | if "Background" in world.node_tree.nodes: 165 | world.node_tree.nodes["Background"].inputs[0].default_value = (0.0, 0.0, 0.0, 1) 166 | 167 | scene.render.fps = fps 168 | 169 | scene.frame_current = 1 170 | scene.frame_start = 1 171 | 172 | scene.eevee.use_bloom = True 173 | scene.eevee.bloom_intensity = 0.005 174 | 175 | # set Ambient Occlusion properties 176 | scene.eevee.use_gtao = True 177 | scene.eevee.gtao_distance = 4 178 | scene.eevee.gtao_factor = 5 179 | 180 | scene.eevee.taa_render_samples = 64 181 | 182 | scene.view_settings.look = "Very High Contrast" 183 | 184 | set_1k_square_render_res() 185 | 186 | 187 | def setup_scene(): 188 | fps = 30 189 | loop_seconds = 12 190 | frame_count = fps * loop_seconds 191 | 192 | project_name = "loop_grid" 193 | bpy.context.scene.render.image_settings.file_format = "PNG" 194 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/" 195 | 196 | seed = 0 197 | if seed: 198 | random.seed(seed) 199 | else: 200 | time_seed() 201 | 202 | # Utility Building Blocks 203 | clean_scene() 204 | set_scene_props(fps, loop_seconds) 205 | 206 | loc = (0, 0, 5) 207 | rot = (0, 0, 0) 208 | setup_camera(loc, rot) 209 | 210 | 211 | def enable_import_images_as_planes(): 212 | 213 | loaded_default, loaded_state = addon_utils.check("io_import_images_as_planes") 214 | if not loaded_state: 215 | addon_utils.enable("io_import_images_as_planes") 216 | 217 | 218 | def add_light(): 219 | bpy.ops.object.light_add(type="SUN") 220 | sun = active_object() 221 | sun.data.energy = 2 222 | sun.data.specular_factor = 0 223 | sun.data.use_shadow = False 224 | 225 | 226 | ################################################################ 227 | # helper functions END 228 | ################################################################ 229 | 230 | 231 | def gen_centerpiece(): 232 | pass 233 | 234 | 235 | def main(): 236 | """ 237 | Python code to generate a grid of loop animations from mp4 files 238 | 239 | Tutorial: https://www.youtube.com/watch?v=1toJeVNOdnk 240 | """ 241 | setup_scene() 242 | enable_import_images_as_planes() 243 | gen_centerpiece() 244 | add_light() 245 | 246 | 247 | if __name__ == "__main__": 248 | main() 249 | -------------------------------------------------------------------------------- /working_with_color_palettes/color_palettes_start.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | import os 4 | import random 5 | import time 6 | 7 | import bpy 8 | 9 | ################################################################ 10 | # helper functions BEGIN 11 | ################################################################ 12 | 13 | 14 | def purge_orphans(): 15 | """ 16 | Remove all orphan data blocks 17 | 18 | see this from more info: 19 | https://youtu.be/3rNqVPtbhzc?t=149 20 | """ 21 | if bpy.app.version >= (3, 0, 0): 22 | # run this only for Blender versions 3.0 and higher 23 | bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) 24 | else: 25 | # run this only for Blender versions lower than 3.0 26 | # call purge_orphans() recursively until there are no more orphan data blocks to purge 27 | result = bpy.ops.outliner.orphans_purge() 28 | if result.pop() != "CANCELLED": 29 | purge_orphans() 30 | 31 | 32 | def clean_scene(): 33 | """ 34 | Removing all of the objects, collection, materials, particles, 35 | textures, images, curves, meshes, actions, nodes, and worlds from the scene 36 | 37 | Checkout this video explanation with example 38 | 39 | "How to clean the scene with Python in Blender (with examples)" 40 | https://youtu.be/3rNqVPtbhzc 41 | """ 42 | # make sure the active object is not in Edit Mode 43 | if bpy.context.active_object and bpy.context.active_object.mode == "EDIT": 44 | bpy.ops.object.editmode_toggle() 45 | 46 | # make sure non of the objects are hidden from the viewport, selection, or disabled 47 | for obj in bpy.data.objects: 48 | obj.hide_set(False) 49 | obj.hide_select = False 50 | obj.hide_viewport = False 51 | 52 | # select all the object and delete them (just like pressing A + X + D in the viewport) 53 | bpy.ops.object.select_all(action="SELECT") 54 | bpy.ops.object.delete() 55 | 56 | # find all the collections and remove them 57 | collection_names = [col.name for col in bpy.data.collections] 58 | for name in collection_names: 59 | bpy.data.collections.remove(bpy.data.collections[name]) 60 | 61 | # in the case when you modify the world shader 62 | # delete and recreate the world object 63 | world_names = [world.name for world in bpy.data.worlds] 64 | for name in world_names: 65 | bpy.data.worlds.remove(bpy.data.worlds[name]) 66 | # create a new world data block 67 | bpy.ops.world.new() 68 | bpy.context.scene.world = bpy.data.worlds["World"] 69 | 70 | purge_orphans() 71 | 72 | 73 | def active_object(): 74 | """ 75 | returns the active object 76 | """ 77 | return bpy.context.active_object 78 | 79 | 80 | def time_seed(): 81 | """ 82 | Sets the random seed based on the time 83 | and copies the seed into the clipboard 84 | """ 85 | seed = time.time() 86 | print(f"seed: {seed}") 87 | random.seed(seed) 88 | 89 | # add the seed value to your clipboard 90 | bpy.context.window_manager.clipboard = str(seed) 91 | 92 | return seed 93 | 94 | 95 | def add_ctrl_empty(name=None): 96 | 97 | bpy.ops.object.empty_add(type="PLAIN_AXES", align="WORLD") 98 | empty_ctrl = active_object() 99 | 100 | if name: 101 | empty_ctrl.name = name 102 | else: 103 | empty_ctrl.name = "empty.cntrl" 104 | 105 | return empty_ctrl 106 | 107 | 108 | def make_active(obj): 109 | bpy.ops.object.select_all(action="DESELECT") 110 | obj.select_set(True) 111 | bpy.context.view_layer.objects.active = obj 112 | 113 | 114 | def track_empty(obj): 115 | """ 116 | create an empty and add a 'Track To' constraint 117 | """ 118 | empty = add_ctrl_empty(name=f"empty.tracker-target.{obj.name}") 119 | 120 | make_active(obj) 121 | bpy.ops.object.constraint_add(type="TRACK_TO") 122 | bpy.context.object.constraints["Track To"].target = empty 123 | 124 | return empty 125 | 126 | 127 | def setup_camera(loc, rot): 128 | """ 129 | create and setup the camera 130 | """ 131 | bpy.ops.object.camera_add(location=loc, rotation=rot) 132 | camera = active_object() 133 | 134 | # set the camera as the "active camera" in the scene 135 | bpy.context.scene.camera = camera 136 | 137 | # set the Focal Length of the camera 138 | camera.data.lens = 70 139 | 140 | camera.data.passepartout_alpha = 0.9 141 | 142 | empty = track_empty(camera) 143 | 144 | return empty 145 | 146 | 147 | def set_1080px_square_render_res(): 148 | """ 149 | Set the resolution of the rendered image to 1080 by 1080 150 | """ 151 | bpy.context.scene.render.resolution_x = 1080 152 | bpy.context.scene.render.resolution_y = 1080 153 | 154 | 155 | def set_scene_props(fps, loop_seconds): 156 | """ 157 | Set scene properties 158 | """ 159 | frame_count = fps * loop_seconds 160 | 161 | scene = bpy.context.scene 162 | scene.frame_end = frame_count 163 | 164 | # set the world background to black 165 | world = bpy.data.worlds["World"] 166 | if "Background" in world.node_tree.nodes: 167 | world.node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1) 168 | 169 | scene.render.fps = fps 170 | 171 | scene.frame_current = 1 172 | scene.frame_start = 1 173 | 174 | scene.eevee.use_bloom = True 175 | scene.eevee.bloom_intensity = 0.005 176 | 177 | # set Ambient Occlusion properties 178 | scene.eevee.use_gtao = True 179 | scene.eevee.gtao_distance = 4 180 | scene.eevee.gtao_factor = 5 181 | 182 | scene.eevee.taa_render_samples = 64 183 | 184 | scene.view_settings.look = "Very High Contrast" 185 | 186 | set_1080px_square_render_res() 187 | 188 | 189 | def setup_scene(i=0): 190 | fps = 30 191 | loop_seconds = 12 192 | frame_count = fps * loop_seconds 193 | 194 | project_name = "color_palettes" 195 | bpy.context.scene.render.image_settings.file_format = "FFMPEG" 196 | bpy.context.scene.render.ffmpeg.format = "MPEG4" 197 | bpy.context.scene.render.filepath = f"/tmp/project_{project_name}/loop_{i}.mp4" 198 | 199 | seed = 0 200 | if seed: 201 | random.seed(seed) 202 | else: 203 | time_seed() 204 | 205 | # Utility Building Blocks 206 | clean_scene() 207 | set_scene_props(fps, loop_seconds) 208 | 209 | loc = (0, 0, 15) 210 | rot = (0, 0, 0) 211 | setup_camera(loc, rot) 212 | 213 | context = { 214 | "frame_count": frame_count, 215 | } 216 | 217 | return context 218 | 219 | 220 | def hex_color_str_to_rgba(hex_color: str): 221 | """ 222 | Converting from a color in the form of a hex triplet string (en.wikipedia.org/wiki/Web_colors#Hex_triplet) 223 | to a Linear RGB with an Alpha of 1.0 224 | 225 | Supports: "#RRGGBB" or "RRGGBB" 226 | """ 227 | # remove the leading '#' symbol if present 228 | if hex_color.startswith("#"): 229 | hex_color = hex_color[1:] 230 | 231 | assert len(hex_color) == 6, "RRGGBB is the supported hex color format" 232 | 233 | # extracting the Red color component - RRxxxx 234 | red = int(hex_color[:2], 16) 235 | # dividing by 255 to get a number between 0.0 and 1.0 236 | srgb_red = red / 255 237 | linear_red = convert_srgb_to_linear_rgb(srgb_red) 238 | 239 | # extracting the Green color component - xxGGxx 240 | green = int(hex_color[2:4], 16) 241 | # dividing by 255 to get a number between 0.0 and 1.0 242 | srgb_green = green / 255 243 | linear_green = convert_srgb_to_linear_rgb(srgb_green) 244 | 245 | # extracting the Blue color component - xxxxBB 246 | blue = int(hex_color[4:6], 16) 247 | # dividing by 255 to get a number between 0.0 and 1.0 248 | srgb_blue = blue / 255 249 | linear_blue = convert_srgb_to_linear_rgb(srgb_blue) 250 | 251 | alpha = 1.0 252 | return tuple([linear_red, linear_green, linear_blue, alpha]) 253 | 254 | 255 | def convert_srgb_to_linear_rgb(srgb_color_component): 256 | """ 257 | Converting from sRGB to Linear RGB 258 | based on https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ 259 | """ 260 | if srgb_color_component <= 0.04045: 261 | linear_color_component = srgb_color_component / 12.92 262 | else: 263 | linear_color_component = math.pow((srgb_color_component + 0.055) / 1.055, 2.4) 264 | 265 | return linear_color_component 266 | 267 | 268 | ################################################################ 269 | # helper functions END 270 | ################################################################ 271 | 272 | 273 | def create_centerpiece(context): 274 | pass 275 | 276 | 277 | def main(): 278 | """ 279 | Python code to generate a scene with 5 planes 280 | and apply a random 5 color palette to them 281 | """ 282 | context = setup_scene() 283 | create_centerpiece(context) 284 | 285 | 286 | if __name__ == "__main__": 287 | main() 288 | --------------------------------------------------------------------------------