├── kernel.json ├── LICENSE ├── README.md └── KernelFile.py /kernel.json: -------------------------------------------------------------------------------- 1 | { 2 | "argv": ["KERNEL_DIR/blender/KernelFile.py", 3 | "-f", "{connection_file}"], 4 | "display_name": "Blender", 5 | "language": "python" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cameron Franz 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 | # About 2 | 3 | Blender has a Python scripting API, but it is limited to an interpreter launched within Blender. This Jupyter kernel starts a Blender instance with an async kernel running inside of it. This means that you can script from [hydrogen](https://github.com/nteract/hydrogen) or a [Notebook](https://jupyter.org/) while still using the Blender UI. 4 | 5 | # Installation 6 | 7 | ## Automatic 8 | 9 | Cheng-chi made a comprehensive installer at https://github.com/cheng-chi/blender_notebook hosted on pip! Instructions are [there](https://github.com/cheng-chi/blender_notebook) 10 | 11 | ## Manual 12 | 13 | Find your `DATA_DIR` with `jupyter --data-dir`. Your `KERNEL_DIR` is `DATA_DIR/kernels/` 14 | 15 | 1. `git clone https://github.com/cameronfr/BlenderKernel` 16 | 1. In `kernel.json`, replace `KERNEL_DIR` with your `KERNEL_DIR` 17 | 2. In `KernelFile.py`, replace `pythonPath` with a python site-packages path (e.g. run `python -c 'import site; print(site.getsitepackages())'` to find it) 18 | 3. In `KernelFile.py`, replace `blenderPath` with a path to your Blender executable (e.g. `/Applications/Blender.app/Contents/MacOS/Blender` on Mac) 19 | 2. `mv BlenderKernel KERNEL_DIR/blender` 20 | 21 | Use `jupyter kernelspec list` to make sure the kernel is listed, then launch the kernel as you would any other. 22 | 23 | To test on the default cube scene, 24 | - run `import bpy` 25 | - the Cube should move when you run `bpy.data.objects["Cube"].location[0] += 0.5` . 26 | 27 | # Caveats 28 | - the kernel should persist when opening other .blend files 29 | - however, if you encounter an error like `ReferenceError: StructRNA of type Object has been removed`, the solution is to `del obj` where `obj` is something you assigned to a Blender object in a previously opened file. 30 | -------------------------------------------------------------------------------- /KernelFile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import subprocess 3 | import sys 4 | import tempfile 5 | 6 | pythonPath = "PYTHON_SITE_PACKAGES_PATH" 7 | blenderPath = "BLENDER_PATH" 8 | kernelFileArgs = sys.argv[1:] # jupyter passes important args defined in kernelspec 9 | 10 | kernelPluginScript = f""" 11 | import asyncio 12 | import sys 13 | # Must append system python path with ipykernel etc. 14 | sys.path.append("{pythonPath}") 15 | import bpy 16 | from ipykernel.kernelapp import IPKernelApp 17 | from bpy.app.handlers import persistent 18 | 19 | class JupyterKernelLoop(bpy.types.Operator): 20 | bl_idname = "asyncio.jupyter_kernel_loop" 21 | bl_label = "Jupyter Kernel Loop" 22 | 23 | _timer = None 24 | 25 | kernelApp = None 26 | 27 | def modal(self, context, event): 28 | 29 | if event.type == 'TIMER': 30 | loop = asyncio.get_event_loop() 31 | loop.call_soon(loop.stop) 32 | loop.run_forever() 33 | 34 | return {{'PASS_THROUGH'}} 35 | 36 | def execute(self, context): 37 | 38 | wm = context.window_manager 39 | self._timer = wm.event_timer_add(0.016, window=context.window) 40 | wm.modal_handler_add(self) 41 | 42 | if not JupyterKernelLoop.kernelApp: 43 | JupyterKernelLoop.kernelApp = IPKernelApp.instance() 44 | JupyterKernelLoop.kernelApp.initialize({['python'] + kernelFileArgs}) 45 | JupyterKernelLoop.kernelApp.kernel.start() # doesn't start event loop, kernelApp.start() does 46 | 47 | return {{'RUNNING_MODAL'}} 48 | 49 | def cancel(self, context): 50 | wm = context.window_manager 51 | wm.event_timer_remove(self._timer) 52 | 53 | class TmpTimer(bpy.types.Operator): 54 | bl_idname = "asyncio.tmp_timer" 55 | bl_label = "TMP Timer" 56 | 57 | _timer = None 58 | 59 | def modal(self, context, event): 60 | 61 | if event.type == 'TIMER': 62 | bpy.ops.asyncio.jupyter_kernel_loop() 63 | self.cancel(context) 64 | 65 | return {{'FINISHED'}} 66 | 67 | def execute(self, context): 68 | 69 | wm = context.window_manager 70 | self._timer = wm.event_timer_add(0.016, window=context.window) 71 | wm.modal_handler_add(self) 72 | 73 | return {{'RUNNING_MODAL'}} 74 | 75 | def cancel(self, context): 76 | wm = context.window_manager 77 | wm.event_timer_remove(self._timer) 78 | 79 | bpy.utils.register_class(JupyterKernelLoop) 80 | bpy.utils.register_class(TmpTimer) 81 | 82 | # start Kernel and asyncio on initial load 83 | bpy.ops.asyncio.tmp_timer() 84 | 85 | # start asyncio on any successive loads 86 | @persistent 87 | def loadHandler(dummy): 88 | bpy.ops.asyncio.jupyter_kernel_loop() 89 | # If call tmp_timer here instead, kernel doesn't work on successive files if have used kernel in current file. 90 | bpy.app.handlers.load_post.append(loadHandler) 91 | 92 | # Need the timer hack because if immediately call registered operation, get 93 | # self.user_global_ns is None error in IPython/core/interactiveshell.py 94 | # The bpy.app.timers causes a segfault when used with jupyter_kernel_loop() 95 | """ 96 | 97 | scriptFile = tempfile.NamedTemporaryFile(suffix=".py") 98 | scriptFile.write(kernelPluginScript.encode("utf-8")) 99 | scriptFile.flush() 100 | subprocess.run([blenderPath, "-P", scriptFile.name]) 101 | --------------------------------------------------------------------------------