Skip to content

Commit

Permalink
feat(external_bpy): Support for using external bpy module
Browse files Browse the repository at this point in the history
- this allows usage of bpy provided from the external python environment
specified by the USE_EXTERNAL_BPY_MODULE environment
- allows using BlenderProc within standard python scripts (no need to run it through blenderproc run)
- debug mode is not allowed with this flag
- there might be limitations originating from this usage of bpy https://docs.blender.org/api/current/info_advanced_blender_as_bpy.html#limitations

- installation of Blender's python environment pip (DefaultConfig.default_pip_packages) dependencies has to be handled manually
  • Loading branch information
Griperis committed Nov 7, 2024
1 parent 08759af commit 2b5e1f2
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 74 deletions.
23 changes: 20 additions & 3 deletions blenderproc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
import os
import sys
from .version import __version__
from .python.utility.SetupUtility import SetupUtility, is_using_external_bpy_module

# If "USE_EXTERNAL_BPY_MODULE" is set, we expect the bpy module is provided from the outside
if is_using_external_bpy_module():
try:
import bpy
if bpy.app.version[0] != 4 and bpy.app.version[1] != 2:
raise RuntimeError("\n###############\n\tUSE_EXTERNAL_BPY_MODULE is set, but bpy module is not from Blender 4.2.\n###############\n")

print("BlenderProc is using external 'bpy' module found in the environment.")
# If we successfully imported bpy of correct version, we can signal that we are in the internal blender python environment
os.environ.setdefault("INSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT", "1")
except ImportError:
raise RuntimeError("\n###############\n\tUSE_EXTERNAL_BPY_MODULE is set, but bpy module could not be imported. Make sure bpy module is present in your python environment.\n###############\n")


# check the python version, only python 3.X is allowed:
if sys.version_info.major < 3:
Expand All @@ -19,7 +34,6 @@
# Also clean the python path as this might disturb the pip installs
if "PYTHONPATH" in os.environ:
del os.environ["PYTHONPATH"]
from .python.utility.SetupUtility import SetupUtility
SetupUtility.setup([])
from .api import loader
from .api import utility
Expand Down Expand Up @@ -50,15 +64,18 @@
if sys.platform == "win32":
is_bproc_shell_called = file_names_of_stack[2] in ["metadata.py", "__main__.py"]
is_command_line_script_called = file_names_of_stack[0] == "command_line.py"
# TODO: Is this right? blenderproc run command when installed from pip -e requires this
is_blenderproc_script = file_names_of_stack[0] == "blenderproc-script.py"

is_correct_startup_command = is_bproc_shell_called or is_command_line_script_called or is_module_call
is_correct_startup_command = is_bproc_shell_called or is_command_line_script_called or is_module_call or is_blenderproc_script
else:
is_bproc_shell_called = file_names_of_stack[0] in ["blenderproc", "command_line.py"]
# check if the name of this file is either blenderproc or if the
# "OUTSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT_BUT_IN_RUN_SCRIPT" is set, which is set in the cli.py
is_correct_startup_command = is_bproc_shell_called or is_module_call

if "OUTSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT_BUT_IN_RUN_SCRIPT" not in os.environ \
and not is_correct_startup_command:
and not is_correct_startup_command and not is_using_external_bpy_module():
# pylint: disable=consider-using-f-string
raise RuntimeError("\n###############\nThis script can only be run by \"blenderproc run\", instead of calling:"
"\n\tpython {}\ncall:\n\tblenderproc run {}\n###############".format(sys.argv[0],
Expand Down
112 changes: 63 additions & 49 deletions blenderproc/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
sys.path.append(repo_root_directory)

# pylint: disable=wrong-import-position
from blenderproc.python.utility.SetupUtility import SetupUtility
from blenderproc.python.utility.SetupUtility import SetupUtility, is_using_external_bpy_module
from blenderproc.python.utility.InstallUtility import InstallUtility
# pylint: enable=wrong-import-position

Expand Down Expand Up @@ -129,14 +129,6 @@ def cli():
# pylint: enable=import-outside-toplevel
print(__version__)
elif args.mode in ["run", "debug", "quickstart"]:

# Install blender, if not already done
determine_result = InstallUtility.determine_blender_install_path(args)
custom_blender_path, blender_install_path = determine_result
blender_run_path, major_version = InstallUtility.make_sure_blender_is_installed(custom_blender_path,
blender_install_path,
args.reinstall_blender)

# Setup script path that should be executed
if args.mode == "quickstart":
path_src_run = os.path.join(repo_root_directory, "blenderproc", "scripts", "quickstart.py")
Expand All @@ -154,51 +146,73 @@ def cli():
# this is done to enable the import of blenderproc inside the blender internal python environment
used_environment["INSIDE_OF_THE_INTERNAL_BLENDER_PYTHON_ENVIRONMENT"] = "1"

# If pip update is forced, remove pip package cache
if args.force_pip_update:
SetupUtility.clean_installed_packages_cache(os.path.dirname(blender_run_path), major_version)

# Run either in debug or in normal mode
if args.mode == "debug":
# pylint: disable=consider-using-with
p = subprocess.Popen([blender_run_path, "--python-use-system-env", "--python-exit-code", "0", "--python",
os.path.join(repo_root_directory, "blenderproc/debug_startup.py"), "--",
path_src_run, temp_dir] + unknown_args,
env=used_environment)
# pylint: enable=consider-using-with
# TODO handle the setup the same way as for Blender
if is_using_external_bpy_module():
# Import the given python script to execute it in blenderproc environment
script_directory = os.path.dirname(path_src_run)
try:
sys.path.append(script_directory)
import importlib
importlib.import_module(os.path.basename(path_src_run).replace(".py", ""))
except ImportError as e:
print(f"Failed to import script for execution: {path_src_run} ")
sys.exit(1)
finally:
sys.path.remove(script_directory)
else:
# pylint: disable=consider-using-with
p = subprocess.Popen([blender_run_path, "--background", "--python-use-system-env", "--python-exit-code",
"2", "--python", path_src_run, "--", args.file, temp_dir] + unknown_args,
env=used_environment)
# pylint: enable=consider-using-with

def clean_temp_dir():
# If temp dir should not be kept and temp dir still exists => remove it
if not args.keep_temp_dir and os.path.exists(temp_dir):
print("Cleaning temporary directory")
shutil.rmtree(temp_dir)

# Listen for SIGTERM signal, so we can properly clean up and terminate the child process
def handle_sigterm(_signum, _frame):
clean_temp_dir()
p.terminate()
# Install blender, if not already done
custom_blender_path, blender_install_path = InstallUtility.determine_blender_install_path(args)
blender_run_path, major_version = InstallUtility.make_sure_blender_is_installed(custom_blender_path,
blender_install_path,
args.reinstall_blender)
# If pip update is forced, remove pip package cache
if args.force_pip_update:
SetupUtility.clean_installed_packages_cache(os.path.dirname(blender_run_path), major_version)

# Run either in debug or in normal mode
if args.mode == "debug":
if is_using_external_bpy_module():
raise RuntimeError("Debug mode is not supported when using 'USE_EXTERNAL_BPY_MODULE'.")

# pylint: disable=consider-using-with
p = subprocess.Popen([blender_run_path, "--python-use-system-env", "--python-exit-code", "0", "--python",
os.path.join(repo_root_directory, "blenderproc/debug_startup.py"), "--",
path_src_run, temp_dir] + unknown_args,
env=used_environment)
# pylint: enable=consider-using-with
else:
# pylint: disable=consider-using-with
p = subprocess.Popen([blender_run_path, "--background", "--python-use-system-env", "--python-exit-code",
"2", "--python", path_src_run, "--", args.file, temp_dir] + unknown_args,
env=used_environment)
# pylint: enable=consider-using-with

def clean_temp_dir():
# If temp dir should not be kept and temp dir still exists => remove it
if not args.keep_temp_dir and os.path.exists(temp_dir):
print("Cleaning temporary directory")
shutil.rmtree(temp_dir)

# Listen for SIGTERM signal, so we can properly clean up and terminate the child process
def handle_sigterm(_signum, _frame):
clean_temp_dir()
p.terminate()

signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGTERM, handle_sigterm)

try:
p.wait()
except KeyboardInterrupt:
try:
p.terminate()
except OSError:
pass
p.wait()

# Clean up
clean_temp_dir()
p.wait()
except KeyboardInterrupt:
try:
p.terminate()
except OSError:
pass
p.wait()

# Clean up
clean_temp_dir()

sys.exit(p.returncode)
sys.exit(p.returncode)
# Import the required entry point
elif args.mode in ["vis", "extract", "download"]:
# pylint: disable=import-outside-toplevel
Expand Down
5 changes: 3 additions & 2 deletions blenderproc/python/utility/Initializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from blenderproc.python.utility.GlobalStorage import GlobalStorage
from blenderproc.python.utility.Utility import reset_keyframes
from blenderproc.python.utility.SetupUtility import is_using_external_bpy_module
from blenderproc.python.camera import CameraUtility
from blenderproc.python.utility.DefaultConfig import DefaultConfig
from blenderproc.python.renderer import RendererUtility
Expand All @@ -30,8 +31,8 @@ def init(clean_up_scene: bool = True):
if clean_up_scene:
clean_up(clean_up_camera=True)

# Set language if necessary
if bpy.context.preferences.view.language != "en_US":
# Set language if necessary if not using external bpy (that has only DEFAULT language)
if not is_using_external_bpy_module() and bpy.context.preferences.view.language != "en_US":
print("Setting blender language settings to english during this run")
bpy.context.preferences.view.language = "en_US"

Expand Down
9 changes: 8 additions & 1 deletion blenderproc/python/utility/InstallUtility.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import contextlib

# pylint: disable=wrong-import-position
from blenderproc.python.utility.SetupUtility import SetupUtility
from blenderproc.python.utility.SetupUtility import SetupUtility, is_using_external_bpy_module
# pylint: enable=wrong-import-position


Expand All @@ -37,6 +37,9 @@ def determine_blender_install_path(used_args: "argparse.NameSpace") -> Union[str
- The path to an already existing blender installation that should be used, otherwise None
- The path to where blender should be installed.
"""
if is_using_external_bpy_module():
return None, None

custom_blender_path = used_args.custom_blender_path
blender_install_path = used_args.blender_install_path

Expand All @@ -59,6 +62,10 @@ def make_sure_blender_is_installed(custom_blender_path: Optional[str], blender_i
- The path to the blender binary.
- The major version of the blender installation.
"""
if is_using_external_bpy_module():
import bpy
return None, str(bpy.app.version[0])

# If blender should be downloaded automatically
if custom_blender_path is None:
# Determine path where blender should be installed
Expand Down
71 changes: 52 additions & 19 deletions blenderproc/python/utility/SetupUtility.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@
from blenderproc.python.utility.DefaultConfig import DefaultConfig


def is_using_external_bpy_module():
"""Returns True if the external bpy module is used, False otherwise.
At this point we don't check whether 'bpy' is available, this is handled in the first lines of
__init__.py. When using external by module, we assume it's available all the time as the
script had to fail before.
If using blenderproc's Blender installation, setup goes through the InstallUtility and
SetupUtility.
"""
return os.environ.get("USE_EXTERNAL_BPY_MODULE", "0") == "1"


class SetupUtility:
"""
Setup class, ensures that all necessary pip packages are there
Expand Down Expand Up @@ -45,26 +58,32 @@ def setup(user_required_packages: Optional[List[str]] = None, blender_path: Opti
:param debug_args: Can be used to overwrite sys.argv in debug mode.
:return: List of sys.argv after removing blender specific commands
"""
packages_path = SetupUtility.setup_pip(user_required_packages, blender_path, major_version, reinstall_packages)

if not SetupUtility.main_setup_called:
SetupUtility.main_setup_called = True
sys.path.append(packages_path)
is_debug_mode = "--background" not in sys.argv
if not is_using_external_bpy_module():
packages_path = SetupUtility.setup_pip(user_required_packages, blender_path, major_version, reinstall_packages)
if not SetupUtility.main_setup_called:
SetupUtility.main_setup_called = True
sys.path.append(packages_path)
is_debug_mode = "--background" not in sys.argv

# Setup temporary directory
if is_debug_mode:
SetupUtility.setup_utility_paths("examples/debugging/temp")
else:
SetupUtility.setup_utility_paths(sys.argv[sys.argv.index("--") + 2])

# Only prepare args in non-debug mode (In debug mode the arguments are already ready to use)
if not is_debug_mode:
# Cut off blender specific arguments
sys.argv = sys.argv[sys.argv.index("--") + 1:sys.argv.index("--") + 2] + \
sys.argv[sys.argv.index("--") + 3:]
elif debug_args is not None:
sys.argv = ["debug"] + debug_args
# Setup temporary directory
if is_debug_mode:
SetupUtility.setup_utility_paths("examples/debugging/temp")
else:
SetupUtility.setup_utility_paths(sys.argv[sys.argv.index("--") + 2])

# Only prepare args in non-debug mode (In debug mode the arguments are already ready to use)
if not is_debug_mode:
# Cut off blender specific arguments
sys.argv = sys.argv[sys.argv.index("--") + 1:sys.argv.index("--") + 2] + \
sys.argv[sys.argv.index("--") + 3:]
elif debug_args is not None:
sys.argv = ["debug"] + debug_args
else:
SetupUtility.setup_utility_paths("blenderproc_temp")
# pylint: disable=import-outside-toplevel,cyclic-import
from blenderproc.python.utility.Utility import Utility, resolve_path
# pylint: enable=import-outside-toplevel,cyclic-import

return sys.argv

Expand Down Expand Up @@ -92,7 +111,13 @@ def determine_python_paths(blender_path: Optional[str], major_version: Optional[
- The path to the directory containing custom pip packages installed by BlenderProc
- The path to the directory containing pip packages installed by blender.
"""
# If no bleneder path is given, determine it based on sys.executable
if is_using_external_bpy_module():
python_path = sys.executable
# TODO: Are these correct?
site_packages = os.path.abspath(os.path.join(python_path, "..", "..", "Lib", "site-packages"))
return (python_path, site_packages, site_packages, site_packages)

# If no blender path is given, determine it based on sys.executable
if blender_path is None:
blender_path = os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..", "..", ".."))
major_version = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..", "..")))
Expand Down Expand Up @@ -145,6 +170,11 @@ def setup_pip(user_required_packages: Optional[List[str]] = None, blender_path:
:param install_default_packages: If True, general required python packages are made sure to be installed.
:return: Returns the path to the directory which contains all custom installed pip packages.
"""
# TODO: We could use the same code to install using pip and DefaultConfig, if we
# return the right paths.
if is_using_external_bpy_module():
raise RuntimeError("USE_EXTERNAL_BPY_MODULE is set, work with packages in the external environment directly.")

required_packages = []
# Only install general required packages on first setup_pip call
if SetupUtility.installed_packages is None and install_default_packages:
Expand Down Expand Up @@ -279,6 +309,9 @@ def uninstall_pip_packages(package_names: List[str], blender_path: str, major_ve
:param blender_path: The path to the blender main folder.
:param major_version: The major version string of the blender installation.
"""
if is_using_external_bpy_module():
raise RuntimeError("USE_EXTERNAL_BPY_MODULE is set, work with packages in the external environment directly.")

# Determine python and packages paths
python_bin, _, packages_import_path, _ = SetupUtility.determine_python_paths(blender_path, major_version)

Expand Down

0 comments on commit 2b5e1f2

Please sign in to comment.