├── .dir-locals.el ├── .gitignore ├── README.org ├── backgroundgui.patch ├── build-blender-fedora.sh ├── build-blender-mac.sh ├── deps.edn ├── examples ├── cube │ ├── deps.edn │ └── src │ │ └── cube.clj └── simple-data │ ├── deps.edn │ └── src │ └── simple_data.clj ├── screenshot.png └── src └── blender_clj ├── DirectMapped.java ├── core.clj └── nrepl_cmdline.clj /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-global-options . "-A:nREPL") 2 | (cider-clojure-cli-parameters . "--middleware '%s'")))) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /vendor/ 3 | 4 | .cpcache 5 | .nrepl-port 6 | 7 | /src/blender_clj/DirectMapped.class 8 | 9 | # InterlliJ 10 | /.idea/ 11 | blender-clj.iml 12 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Description 2 | 3 | This project uses libpython-clj and a custom build of blender as a python module, to be able to load it 4 | in my clojure repl and script it with the GUI visible, all from clojure. 5 | 6 | For an alternative approach, which doesn't require a custom build of blender, see https://github.com/tristanstraub/clj-python-trampoline 7 | 8 | * Motivation 9 | 10 | I want to script blender with clojure from cider/emacs, or CLI, etc. I want the GUI to be visible. 11 | 12 | * Screenshot 13 | 14 | [[screenshot.png]] 15 | 16 | * Procedure 17 | 18 | We need blender to be built as the python module "bpy.so", but patched to run the GUI in the foreground. 19 | 20 | The build has been tested on: 21 | 22 | - Fedora 31 (python 3.7.7, blender 2.83) 23 | 24 | * Building 25 | 26 | "bpy.so" can be built from the patched blender source as follows: 27 | 28 | #+BEGIN_SRC sh 29 | ./build-blender-fedora.sh 30 | #+END_SRC 31 | 32 | See https://wiki.blender.org/wiki/Building_Blender/Linux/Fedora for more details regarding dependencies. 33 | 34 | * Running 35 | 36 | To get blender running on the main thread and nrepl in the background, run the following, after which 37 | you can jack-in with your favourite editor. 38 | 39 | #+BEGIN_SRC sh 40 | clj -A:nREPL 41 | #+END_SRC 42 | 43 | For emacs users, ".dir-locals.el" exists to make jacking in work as expected, along with cider middleware. 44 | 45 | * Demo 46 | 47 | After building, open a clojure repl with the following command: 48 | 49 | #+BEGIN_SRC sh 50 | ./build-blender-fedora.sh repl 51 | #+END_SRC 52 | 53 | Run the demo: 54 | 55 | #+BEGIN_SRC clojure 56 | (require 'blender-clj.examples.cube) 57 | 58 | (blender-clj.examples.cube/demo) 59 | #+END_SRC 60 | 61 | Alternatively, a repl can be loaded as follows, where BPY_BIN_PATH is the directory where bpy.so can be found. 62 | 63 | #+BEGIN_SRC 64 | BPY_BIN_PATH=vendor/blender-git/build_linux/bin clj -r 65 | #+END_SRC 66 | -------------------------------------------------------------------------------- /backgroundgui.patch: -------------------------------------------------------------------------------- 1 | diff --git a/source/blender/python/intern/bpy.c b/source/blender/python/intern/bpy.c 2 | index de8fd87db58..e004e2c1a28 100644 3 | --- a/source/blender/python/intern/bpy.c 4 | +++ b/source/blender/python/intern/bpy.c 5 | @@ -290,6 +290,19 @@ static PyObject *bpy_escape_identifier(PyObject *UNUSED(self), PyObject *value) 6 | return value_escape; 7 | } 8 | 9 | +void WM_python_main(void); 10 | + 11 | +PyObject * ui_main_ptr(void) { 12 | + return PyCapsule_New((void*)&WM_python_main, "ui_main", NULL); 13 | +} 14 | + 15 | +static PyMethodDef meth_bpy_ui_main = { 16 | + "ui_main", 17 | + (PyCFunction)ui_main_ptr, 18 | + METH_NOARGS, 19 | + NULL, 20 | +}; 21 | + 22 | static PyMethodDef meth_bpy_script_paths = { 23 | "script_paths", 24 | (PyCFunction)bpy_script_paths, 25 | @@ -407,6 +420,10 @@ void BPy_init_modules(void) 26 | /* Register methods and property get/set for RNA types. */ 27 | BPY_rna_types_extend_capi(); 28 | 29 | + PyModule_AddObject(mod, 30 | + meth_bpy_ui_main.ml_name, 31 | + (PyObject *)PyCFunction_New(&meth_bpy_ui_main, NULL)); 32 | + 33 | /* utility func's that have nowhere else to go */ 34 | PyModule_AddObject(mod, 35 | meth_bpy_script_paths.ml_name, 36 | diff --git a/source/creator/creator.c b/source/creator/creator.c 37 | index ea64184c826..d3465a05d07 100644 38 | --- a/source/creator/creator.c 39 | +++ b/source/creator/creator.c 40 | @@ -214,6 +214,11 @@ char **environ = NULL; 41 | # endif 42 | #endif 43 | 44 | +void WM_python_main(void) 45 | +{ 46 | + WM_main(evil_C); 47 | +} 48 | + 49 | /** 50 | * Blender's main function responsibilities are: 51 | * - setup subsystems. 52 | @@ -419,7 +424,7 @@ int main(int argc, 53 | 54 | #if defined(WITH_PYTHON_MODULE) || defined(WITH_HEADLESS) 55 | /* Python module mode ALWAYS runs in background-mode (for now). */ 56 | - G.background = true; 57 | + G.background = false; 58 | #else 59 | if (G.background) { 60 | main_signal_setup_background(); 61 | -------------------------------------------------------------------------------- /build-blender-fedora.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euxo pipefail 3 | 4 | # https://wiki.blender.org/wiki/Building_Blender/Linux/Fedora 5 | 6 | function get-blender { 7 | mkdir -p vendor/blender-git 8 | cd vendor/blender-git 9 | git clone https://git.blender.org/blender.git 10 | } 11 | 12 | function get-libraries { 13 | mkdir -p vendor/blender-git/lib 14 | cd vendor/blender-git/lib 15 | svn checkout https://svn.blender.org/svnroot/bf-blender/trunk/lib/linux_centos7_x86_64 16 | } 17 | 18 | function build-module { 19 | cd vendor/blender-git/blender 20 | make update 21 | patch -p1 < ../../../backgroundgui.patch 22 | BUILD_CMAKE_ARGS="-DWITH_MEM_JEMALLOC=OFF -DWITH_PYTHON_INSTALL=OFF -DWITH_AUDASPACE=OFF -DWITH_PYTHON_MODULE=ON" make 23 | } 24 | 25 | function build-jna-mapper { 26 | mkdir -p lib 27 | (cd lib; curl -L https://github.com/java-native-access/jna/archive/5.5.0.tar.gz | tar -xvzf -) 28 | 29 | javac -cp ./lib/jna-5.5.0/dist/jna.jar src/blender_clj/DirectMapped.java 30 | } 31 | 32 | function server { 33 | BPY_BIN_PATH=vendor/blender-git/build_linux/bin clj -A:nREPL | grep -o "nrepl://[^:]*:.*" | cut -d ":" -f 3 > .nrepl-port 34 | } 35 | 36 | function repl { 37 | rm -f .nrepl-port 38 | (server &) 39 | while [ ! -e .nrepl-port ] || [ -z "$(cat .nrepl-port)" ]; do 40 | sleep 1 41 | done 42 | BPY_BIN_PATH=vendor/blender-git/build_linux/bin clj -R:nREPL -m nrepl.cmdline -c -p $(cat .nrepl-port) 43 | 44 | } 45 | 46 | if [ -z "${1:-}" ]; then 47 | (get-blender) 48 | (get-libraries) 49 | (build-module) 50 | (build-jna-mapper) 51 | else 52 | $@ 53 | fi 54 | -------------------------------------------------------------------------------- /build-blender-mac.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | # https://wiki.blender.org/wiki/Building_Blender/Mac 5 | 6 | # Dependencies: 7 | # 1. install Xcode Development Tools 8 | # 2. install python 3.7 from https://www.python.org/downloads/release/python-377 9 | # Under my 10.15.5: 10 | # - I used their macOS installer, it should install itself to /Library/Frameworks/Python.framework/Versions/3.7 11 | # - had to do `ln -s /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/config-3.7m-darwin /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/config-3.7m` 12 | # 3. install homebrew => https://brew.sh 13 | # 4. run/see install_deps below 14 | # 15 | # Getting Blender: 16 | # 1. export BLENDER_GIT_DIR, e.g. set it to "$ROOT_DIR/vendor/blender-git" 17 | # 2. git clone https://git.blender.org/blender.git "$BLENDER_GIT_DIR" 18 | # 3. note that Blender's make process will create lib and build_ directory in parent of "$BLENDER_GIT_DIR" 19 | # effectively BLENDER_LIB_DIR="$BLENDER_GIT_DIR/../lib" 20 | # effectively BLENDER_BUILD_DIR="$BLENDER_GIT_DIR/../build_darwin" 21 | 22 | set -x 23 | 24 | cd "$(dirname "${BASH_SOURCE[0]}")" 25 | 26 | ROOT_DIR="$(pwd)" 27 | OUR_LIB_DIR="$ROOT_DIR/lib" 28 | BLENDER_GIT_DIR=${BLENDER_GIT_DIR:-./vendor/blender-git} 29 | BLENDER_LIB_DIR="$BLENDER_GIT_DIR/../lib" 30 | BLENDER_BUILD_DIR="$BLENDER_GIT_DIR/../build_darwin" 31 | JNA_VERSION=${JNA_VERSION:-"5.5.0"} 32 | 33 | ensure_lib_dir() { 34 | if [[ ! -d "$OUR_LIB_DIR" ]]; then 35 | mkdir -p "$OUR_LIB_DIR" 36 | fi 37 | } 38 | 39 | install_deps() { 40 | brew install git svn cmake 41 | # and maybe more... haven't tested this on a clean system 42 | } 43 | 44 | build_module() { 45 | cd "$BLENDER_GIT_DIR" 46 | 47 | # remove previously applied patches and intermediate files 48 | git clean -fd 49 | git reset --hard HEAD 50 | 51 | git submodule update --init --recursive 52 | 53 | make update 54 | patch -p1 <"$ROOT_DIR/backgroundgui.patch" 55 | CMAKE_ARGS=( 56 | # TODO: I had to disable openMP support under macOS, it looks like Blender's make does not properly link to 57 | # $BLENDER_LIB_DIR/darwin/openmp/lib/libomp.dylib 58 | # I didn't figure out how to fix it, so I simply disable it for now 59 | "-DWITH_OPENMP=OFF" 60 | "-DWITH_MEM_JEMALLOC=OFF" 61 | "-DWITH_PYTHON_INSTALL=OFF" 62 | "-DWITH_AUDASPACE=OFF" 63 | "-DWITH_PYTHON_MODULE=ON" 64 | ) 65 | export BUILD_CMAKE_ARGS="${CMAKE_ARGS[*]}" 66 | make 67 | } 68 | 69 | build_jna_mapper() { 70 | ensure_lib_dir 71 | TMP_JNA_LIB=/tmp/jna.tar.gz 72 | if [[ ! -f "$TMP_JNA_LIB" ]]; then 73 | curl -L "https://github.com/java-native-access/jna/archive/$JNA_VERSION.tar.gz" -o "$TMP_JNA_LIB" 74 | fi 75 | tar -C "$OUR_LIB_DIR" -xvzf "$TMP_JNA_LIB" 76 | javac -cp "$OUR_LIB_DIR/jna-$JNA_VERSION/dist/jna.jar" "$ROOT_DIR/src/blender_clj/DirectMapped.java" 77 | } 78 | 79 | repl() { 80 | export BPY_BIN_PATH="$BLENDER_BUILD_DIR/bin" 81 | exec clj -r 82 | } 83 | 84 | if [[ $# -eq 0 ]]; then 85 | build_module 86 | build_jna_mapper 87 | else 88 | "$1" 89 | fi 90 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "vendor"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 3 | clj-python/libpython-clj {:mvn/version "1.44"}} 4 | 5 | :aliases {:nREPL {:extra-deps {nrepl/nrepl {:mvn/version "0.7.0"}} 6 | :main-opts ["-m" "blender-clj.nrepl-cmdline"]}}} 7 | -------------------------------------------------------------------------------- /examples/cube/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 3 | blender-clj {:local/root "../../"}} 4 | 5 | :aliases {:nREPL {:main-opts ["-m" "blender-clj.nrepl-cmdline"]}}} 6 | -------------------------------------------------------------------------------- /examples/cube/src/cube.clj: -------------------------------------------------------------------------------- 1 | (ns cube 2 | (:require [blender-clj.core :as blender] 3 | [libpython-clj.python :refer [py..] :as py])) 4 | 5 | (defn demo 6 | [] 7 | (let [bpy (py/import-module "bpy")] 8 | (blender/with-context 9 | (fn [defaults] 10 | (dotimes [_ 10] 11 | (py.. bpy -ops -mesh (primitive_cube_add defaults :size 3 :location (vec (repeatedly 3 #(- (rand-int 20) 10)))))))))) 12 | 13 | (comment 14 | (demo) 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /examples/simple-data/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 3 | blender-clj {:local/root "../../"}} 4 | 5 | :aliases {:nREPL {:main-opts ["-m" "blender-clj.nrepl-cmdline"]}}} 6 | -------------------------------------------------------------------------------- /examples/simple-data/src/simple_data.clj: -------------------------------------------------------------------------------- 1 | (ns blender-clj.examples.simple-data 2 | (:require [blender-clj.core :as blender] 3 | [libpython-clj.python :refer [py..] :as py])) 4 | 5 | (def ^:dynamic *bpy* 6 | nil) 7 | 8 | (defmulti bpy-modifier-add 9 | (fn [defaults config] 10 | (:modifier/type config))) 11 | 12 | (defmethod bpy-modifier-add :array 13 | [defaults {offset-count :count :keys [use-object-offset offset-object] :as config}] 14 | (py.. *bpy* -ops -object (modifier_add defaults :type "ARRAY")) 15 | (let [modifier (py.. *bpy* -context -view_layer -objects -active -modifiers (get "Array"))] 16 | (when use-object-offset 17 | (py/set-attr! modifier 18 | "use_object_offset" 19 | use-object-offset)) 20 | (when offset-object 21 | (py/set-attr! modifier 22 | "offset_object" 23 | (py.. *bpy* -data -objects (get (:name offset-object))))) 24 | 25 | (when offset-object 26 | (py/set-attr! modifier 27 | "offset_object" 28 | (py.. *bpy* -data -objects (get (:name offset-object))))) 29 | (when offset-count 30 | (py/set-attr! modifier "count" offset-count)))) 31 | 32 | (defmulti bpy-primitive-add 33 | (fn [defaults config] 34 | (:primitive config))) 35 | 36 | (defmethod bpy-primitive-add :cube 37 | [defaults {object-name :name :keys [location rotation modifiers] :as config}] 38 | (py.. *bpy* -ops -mesh 39 | (primitive_cube_add defaults :location location)) 40 | (let [object (py.. *bpy* -context -view_layer -objects -active)] 41 | (when object-name 42 | (py/set-attr! object "name" object-name)) 43 | (when rotation 44 | (py.. *bpy* -ops -transform (rotate defaults :value (:value rotation))))) 45 | 46 | (run! (partial bpy-modifier-add defaults) modifiers)) 47 | 48 | (defmethod bpy-primitive-add :cylinder 49 | [defaults {object-name :name :keys [location modifiers] :as config}] 50 | (py.. *bpy* -ops -mesh 51 | (primitive_cylinder_add defaults :location location)) 52 | (when object-name 53 | (py/set-attr! (py.. *bpy* -context -view_layer -objects -active) "name" object-name)) 54 | 55 | (run! (partial bpy-modifier-add defaults) modifiers)) 56 | 57 | (defmulti bpy-add 58 | (fn [defaults config] 59 | (:type config))) 60 | 61 | (defmethod bpy-add :primitive 62 | [defaults config] 63 | (bpy-primitive-add defaults config)) 64 | 65 | (defn delete-all 66 | [] 67 | (blender/with-context 68 | (fn [defaults] 69 | (binding [*bpy* (py/import-module "bpy")] 70 | (py.. *bpy* -ops -object (select_all defaults :action "SELECT")) 71 | (py.. *bpy* -ops -object (delete defaults)))))) 72 | 73 | (defn demo 74 | [] 75 | (let [scene [{:type :primitive 76 | :name "cube-1" 77 | :primitive :cube 78 | :location [0 0 0] 79 | :rotation {:value 1.0}} 80 | {:type :primitive 81 | :name "cube-2" 82 | :primitive :cube 83 | :location [10 0 0]} 84 | {:type :primitive 85 | :name "cylinder-1" 86 | :primitive :cylinder 87 | :location [5 0 0] 88 | :modifiers [{:type :modifier 89 | :modifier/type :array 90 | :use-object-offset true 91 | :offset-object {:name "cube-1"} 92 | :count 5}]}]] 93 | 94 | (delete-all) 95 | 96 | (blender/with-context 97 | (fn [defaults] 98 | (binding [*bpy* (py/import-module "bpy")] 99 | (run! (partial bpy-add defaults) scene)))))) 100 | 101 | (comment 102 | (demo) 103 | 104 | ) 105 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tristanstraub/blender-clj/f32c21c7df4fe059586ca79c1e689b373f16dee1/screenshot.png -------------------------------------------------------------------------------- /src/blender_clj/DirectMapped.java: -------------------------------------------------------------------------------- 1 | package blender_clj; 2 | 3 | import com.sun.jna.Pointer; 4 | 5 | public class DirectMapped 6 | { 7 | public static native Pointer PyCapsule_GetPointer(Pointer capsule, String name); 8 | } 9 | -------------------------------------------------------------------------------- /src/blender_clj/core.clj: -------------------------------------------------------------------------------- 1 | (ns blender-clj.core 2 | (:require [libpython-clj.require :refer [require-python import-python]] 3 | [libpython-clj.python :refer [py..] :as py] 4 | [tech.v2.datatype :as dtype] 5 | libpython-clj.jna.protocols.object 6 | [libpython-clj.jna.base :as libpy-base :refer [ensure-pyobj]] 7 | [tech.jna :as jna] 8 | [clojure.java.io :as io]) 9 | (:import [blender_clj DirectMapped])) 10 | 11 | (def bpy-so-path 12 | (or (System/getenv "BPY_BIN_PATH") 13 | (.getAbsolutePath (io/file (io/resource "blender-git/build_linux/bin"))))) 14 | 15 | (defn ui-main 16 | [] 17 | (let [sys (doto (py/import-module "sys") 18 | (py.. -path (append bpy-so-path))) 19 | library (jna/load-library libpy-base/*python-library*)] 20 | (com.sun.jna.Native/register DirectMapped library) 21 | 22 | (let [bpy (py/import-module "bpy") 23 | _bpy (py/import-module "_bpy") 24 | ui-main-ptr (ensure-pyobj (py.. _bpy (ui_main))) 25 | func (com.sun.jna.Function/getFunction (DirectMapped/PyCapsule_GetPointer ui-main-ptr "ui_main"))] 26 | (.invoke func (make-array Object 0))))) 27 | 28 | (defn get-defaults 29 | [] 30 | (let [bpy (py/import-module "bpy") 31 | window (first (seq (py.. bpy -context -window_manager -windows))) 32 | area (first (filter #(= "VIEW_3D" (py.. % -type)) (py.. window -screen -areas))) 33 | region (first (filter #(= "WINDOW" (py.. % -type)) (py.. area -regions))) 34 | workspace (first (filter #(= "Layout" (py.. % -name)) (py.. bpy -data -workspaces))) 35 | ctx (py.. bpy -context (copy))] 36 | (py.. ctx (update (py/->py-dict {"window" window 37 | "screen" (py.. window -screen) 38 | "area" area 39 | "region" region 40 | "workspace" workspace}))) 41 | ctx)) 42 | 43 | (def ^:dynamic *in-timer?* 44 | false) 45 | 46 | (defn with-context-transaction 47 | [p _ f] 48 | (let [bpy (py/import-module "bpy") 49 | timer-fn (fn [] 50 | (try 51 | (deliver p (f (get-defaults))) 52 | (catch Exception e 53 | (deliver p e))) 54 | nil)] 55 | (py.. bpy -app -timers (register (fn [] 56 | (binding [*in-timer?* true] 57 | (timer-fn))))) 58 | p)) 59 | 60 | (defn resolve-promise 61 | [p] 62 | (let [result @p] 63 | (if (instance? Exception result) 64 | (throw result) 65 | result))) 66 | 67 | (defonce with-context-agent 68 | (agent nil)) 69 | 70 | (defn skip-exceptions 71 | [f] 72 | (fn [state & args] 73 | (try 74 | (apply f state args) 75 | (catch Exception e 76 | (println e) 77 | state)))) 78 | 79 | (defn with-context 80 | [f] 81 | (if *in-timer?* 82 | (f (get-defaults)) 83 | (let [p (promise)] 84 | (send with-context-agent (skip-exceptions (partial with-context-transaction p)) f) 85 | (resolve-promise p)))) 86 | -------------------------------------------------------------------------------- /src/blender_clj/nrepl_cmdline.clj: -------------------------------------------------------------------------------- 1 | (ns blender-clj.nrepl-cmdline 2 | (:require [blender-clj.core :refer [ui-main]] 3 | [nrepl.cmdline])) 4 | 5 | (defn -main 6 | [& args] 7 | ;; Run nrepl.cmdline so that --middleware options work 8 | (future (apply nrepl.cmdline/-main args)) 9 | (ui-main)) 10 | --------------------------------------------------------------------------------