diff --git a/.gitignore b/.gitignore index 59a5874..fe9c7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ .DS_Store .vscode/ *.zip +*.whl # the following ignores are used to ignore the local softlink files # the extern folder won't be affected by this @@ -10,4 +11,4 @@ meshio future fileseq -docs/_build/* \ No newline at end of file +docs/_build/* diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..c3c3f96 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,35 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/README.md b/README.md index 9112e76..4c12caa 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,11 @@ After obtaining an installable `.zip` file either from the releases page or from ## 2. How to use -DISCLAIMER: Some of the screenshots may not be up to date with the most recent version of the addon, especially with respect to the text and ordering of UI elements. +**Note**: When rendering the animation, please turn on the `Lock Interface`. This will prevent artifacts from occurring, especially if the user continues to operate the Blender interface during the render process. -After installing addon, you can find it in the toolbar, which is accessible here or toggled by pressing the `n` key. +![lock interface](images/lock.png) + +After installing addon, you can find it in the toolbar, which is accessible here or toggled by pressing the `N` key. ![drag](images/drag.png) @@ -96,84 +98,145 @@ Then you can find it here. ![homepage](images/location.png) -### 2. Load the animation sequence you want +### 1. Load the animation sequence you want + +The easiest way to import sequences is to use the large "Import Sequences" button. After pressing it, you can select as many sequences as you want which will be imported to the scene after pressing `Accept`. + +#### 1.1 Relative Paths + +The first option is the "Relative Path" option which is turned off by default, i.e. it uses absolute paths by default. + +To enable this option, the blender file has to be saved first. Then the sequences that are imported will be referenced using relative paths from the location of the saved `.blend` file. As such, if you move the `.blend` file in conjunction with the data to another directory (keeping their relative locations the same) the sequence loader will still work. This is especially useful when working with cloud synchronized folders, whose absolute paths may be different on different computers. + +To change the "Root Directory" to be somewhere else please have a look at the "Global Settings" section. + +#### 1.2 Import Default Normals + +The sequence loader tries to look for already stored normals that have to meet certain criteria depending on the file types. Currently supported are .obj and .vtk. + +For .obj: +- Normals have to be normalized to 1. Vertex normals as well as face vertex normals (each vertex can have a different normal for each face) are supported. +- Any normals are stored by using "vn". Vertex normals are simply referenced in the same order as the vertices and for face vertex normals, the respective index of the normal is stated in the third position where the vertex is referenced for face (e.g. 1/2/3 or 1//3 would reference the 3rd normal for the 1st vertex in some face). + +For .vtk: +- Only verex normals are supported. They have to be named "normals". + +#### 1.3 Custom Transformation Matrix + +When enabling this option, you can define a custom transformation matrix (using XYZ Euler Angles) that will be applied once when importing a sequence. -You can select the directory in which your data is located through the GUI by clicking the rightmost icon. It will open the default blender file explorer. Then you can go to the directory you want, for example, like image showed below. **You only need navigate to the directory and click "Accept". Files are shown but not selectable in this dialogue.** +#### 1.4 Load sequences from folder (Legacy importer) -![directory](images/directory.png) +You can select the directory in which your data is located through the GUI by clicking the folder icon. It will open the default blender file explorer. Then, when you are in the desired folder, click `Accept`. You can't select any files in this GUI. -Then the addon will automatically try to detect the sequences in this directory, so that you simply select the sequence you want. If the desired sequence is not shown, you can switch to enter a manual pattern, where a single `@` character is used to denote a running frame index. +Then the addon will automatically try to detect the sequences in this directory, so that you simply select the sequence you want. If the desired sequence is not shown, you can enable the "Custom Pattern" option to enter a manual pattern, where a single `@` character is used to denote a running frame index. + +The refresh button simply looks again for sequences in the selcted folder, in case there were any changes made. + +Then click the `Load` Sequence" button to load the selected sequence or the `Load All` button to load all found sequences. ![sequence](images/sequence.png) -#### 2.1 Absolute vs. Relative Paths +### 2. Global Settings -There is a small checkbox about whether to use `relative paths` or not. +#### 2.1 Root Directory -When toggled on, the blender file must be saved before loading the sequence. Then this sequence will be loaded using relative path from the location of the saved `.blend` file. As such, if you move the `.blend` file in conjunction with the data to another directory (keeping their relative locations the same) the sequence loader will still work. This is especially useful when working with cloud synchronized folders, whose absolute paths may be different on different computers. +This is where a new root directory can be set. All relative paths will be relative to this directory. If left empty, the file path of the Blender file will be used. -If toggled off (default), it will use absolute path to load the sequence. For this, the `.blend` file does not have to be saved in advance. +#### 2.2 Print Sequence Information -![relative_path](images/path.png) +Print some useful information during rendering in a file located in the same folder as the render output and in the console. For the latter, Blender has to be started from the console. -#### 2.2 Sequence List View +#### 2.3 Auto Refresh Active Sequences -After the sequence being imported, it will be available in the `Imported Sequences` panel, with more settings being available in `Sequence Settings` panel once a sequence has been selected. +Automatically refresh all active sequences whenever the frame changes. See "Refresh Sequence" for further explanations. This can be useful when generating a sequence and rendering is done simultaneously. -![settings](images/list.png) +#### 2.4 Auto Refresh All Sequences -By default, all supported file formats are simply imported as geometry (a collection of vertices, lines, triangles and quads). As such, you should be able to directly play/render the animation if it contains geometry. +Like the above but with all sequences. -Note: When rendering the animation, please turn on the `Lock Interface`. This will prevent artifacts from occurring, especially if the user continues to operate the Blender interface during the render process. +### 3. Sequence List View -![lock interface](images/lock.png) +After the sequence being imported, it will be available in the `Sequences` panel, with more settings being available in `Sequence Settings` panel once a sequence has been selected. + +![settings](images/list.png) + +For each sequence we show the name, a button that shows whether a sequence is active or inactive (this button is clickable, see the next section for more details on the functionality), the current frame number which is also driver that can be edited as well as the smallest and largest number of the respective sequence. -##### 2.2.1 Enable/ Disable +##### 3.1 Activate / Deactivate Sequences -It is possible to individually enable and disable sequences from updating when the animation frame changes. This is very useful when working with very large files or many sequences as it reduces the computational overhead of loading these sequences. -`Enabled` means, that the sequence will be updated on frame change, and `Disabled` means that the sequence won't be updated on frame change. +It is possible to individually activate or deactivate sequences from updating when the animation frame changes. This is very useful when working with very large files or many sequences as it reduces the computational overhead of loading these sequences. +`Activated` means, that the sequence will be updated on frame change, and `Deactivated` means that the sequence won't be updated on frame change. -##### 2.2.1 Refresh Sequence +##### 3.2 Refresh Sequence `Refresh Sequence` can be useful when the sequence is imported while the data is still being generated and not yet complete. Refreshing the sequence can detect the frames added after being imported. -#### 2.3 Settings +#### 3.3 Activate / Deactivate All -#### 2.3.1 Geometry Nodes +Activate or deactivate all sequences shown in the sequences view. -While all files are imported as plain geometry, we provide some templates that we have found to be incredibly useful for visualizing particle data. +#### 3.4 Set Timeline -Applying the `Point Cloud` geometry node, the vertices of the mesh are converted to a point cloud, which can be rendered only by [cycles](https://docs.blender.org/manual/en/latest/render/cycles/introduction.html) and only as spheres. The exact geometry node setup can be seen in the geometry nodes tab and may be modified as desired, e.g. to set the particle radius. +Sets the Blender timeline to range of the smallest to largest number of a file of the selected sequence. -Applying `Instances` geometry nodes, the vertices of the mesh are converted to cubes, which can be rendered by both [eevee](https://docs.blender.org/manual/en/latest/render/eevee/index.html) and [cycles](https://docs.blender.org/manual/en/latest/render/cycles/introduction.html). The exact geometry node setup can be seen in the geometry nodes tab and may be modified as desired, e.g. to set the particle radius and to change the instanced geometry. **CAUTION: Because this node setup relies on the `Realize Instances` node, the memory usage increases extremely rapidly. Make sure to save the `.blend` file before attempting this, as Blender may run out of memory!!!** +### 4. Sequence Properties -Applying the `Mesh` geometry node will restore the default geometry nodes, which simply display the imported geometry as it is. +#### 4.1 Match Blender Frame Numbers -Notes: +This shows the file of a sequence at the frame number exactly matching the number in the file, otherwise it will not show anything. So if a file sequence goes from 2-10 and 15-30 only at these frames the respective files will be shown. -1. `Instances` is super memory hungry compared with `Point Cloud`. -2. After applying `Point Cloud` or `Instances` geometry nodes, you need to assign the material inside the geometry nodes. So to save your work, you can simply assign the material here, then apply the `Point Cloud` or `Instances` geometry nodes. -3. To access the attributes for shading, use the `Attribute` node in the Shader Editor and simply specify the attribute string. The imported attributes can be seen in the spreadsheet browser of the Geometry Nodes tab and are also listed in the addon UI. +By default this option is turned off and the sequence starts in Blender from 0 and on each following frame the next available file is loaded. For frame number larger than the length of the file sequence, this procedure is looped. -![material](images/geometry_nodes.png) +#### 4.2 Path + +The path of the file sequence is shown here and can also be edited. Relative paths start with // which basically is placeholder for the root directory. + +#### 4.3 Pattern + +Here you can see and edit the pattern of a file sequence that is used to detect the sequence as well as to determine how many frames the sequence has. A pattern consists of the name, then the frame range followed by an @ and at last, the file extension. + +#### 4.4 Current File -#### 2.3.2 Path Information +This is read-only and shows the absolute path of the file that is currenlty loaded from the selected sequence. -This shows the path of the sequence for debugging purposes, however it's not editable. +#### 4.5 Last Loading Time -#### 2.3.3 Attributes Settings +Read-only field, that shows how long it took to load the current file in milliseconds. + +#### 4.6 Attributes Settings This panel shows the available **Vertex Attributes**, it's not editable. Note: In order to avoid conflicts with Blenders built-in attributes, all the attributes names are renamed by prefixing `bseq_`. For example, `id` -> `bseq_id`. Keep this in mind when accessing attributes in the shader editor. -#### 2.3.4 Split Norm per Vertex +#### 4.6.1 Split Norm per Vertex We also provide the ability to use a per-vertex vector attribute as custom normals for shading. For more details check the official documentation [here](https://docs.blender.org/manual/en/latest/modeling/meshes/structure.html#modeling-meshes-normals-custom). Note: the addon does not check if the selected attribute is suitable for normals or not. E.g. if the data type of the attribute is int instead of float, then Blender will simply give a runtime error. -#### 2.3.5 Advanced Settings +### 5. Advanced Settings + +#### 5.1 Script + +Here you can import your own script for loading and preprocessing your file sequences. For more information look at the teplate.py file under Scripting -> Templates -> Sequence Loader -> Template. + +#### 5.2 Geometry Nodes + +While all files are imported as plain geometry, we provide some templates that we have found to be incredibly useful for visualizing particle data. + +Applying the `Point Cloud` geometry node, the vertices of the mesh are converted to a point cloud, which can be rendered only by [cycles](https://docs.blender.org/manual/en/latest/render/cycles/introduction.html) and only as spheres. The exact geometry node setup can be seen in the geometry nodes tab and may be modified as desired, e.g. to set the particle radius. + +Applying `Instances` geometry nodes, the vertices of the mesh are converted to cubes, which can be rendered by both [eevee](https://docs.blender.org/manual/en/latest/render/eevee/index.html) and [cycles](https://docs.blender.org/manual/en/latest/render/cycles/introduction.html). The exact geometry node setup can be seen in the geometry nodes tab and may be modified as desired, e.g. to set the particle radius and to change the instanced geometry. **CAUTION: Because this node setup relies on the `Realize Instances` node, the memory usage increases extremely rapidly. Make sure to save the `.blend` file before attempting this, as Blender may run out of memory!!!** -TODO +Applying the `Mesh` geometry node will restore the default geometry nodes, which simply display the imported geometry as it is. + +Notes: + +1. `Instances` is super memory hungry compared with `Point Cloud`. +2. After applying `Point Cloud` or `Instances` geometry nodes, you need to assign the material inside the geometry nodes. So to save your work, you can simply assign the material here, then apply the `Point Cloud` or `Instances` geometry nodes. +3. To access the attributes for shading, use the `Attribute` node in the Shader Editor and simply specify the attribute string. The imported attributes can be seen in the spreadsheet browser of the Geometry Nodes tab and are also listed in the addon UI. + +![material](images/geometry_nodes.png) diff --git a/__init__.py b/__init__.py index 04429e2..d98c67b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,27 +1,24 @@ -bl_info = { - "name": "Sequence Loader", - "description": "Loader for meshio supported mesh files/ simulation sequences", - "author": "Interactive Computer Graphics", - "version": (0, 3, 0), - "blender": (4, 0, 0), - "warning": "", - "support": "COMMUNITY", - "category": "Import-Export", -} - import bpy import os import sys -current_folder = os.path.dirname(os.path.abspath(__file__)) -if current_folder not in sys.path: - sys.path.append(current_folder) +# current_folder = os.path.dirname(os.path.abspath(__file__)) +# if current_folder not in sys.path: +# sys.path.append(current_folder) +# # add paths of external libraries to sys.path +# if os.path.exists(os.path.join(current_folder, "extern")): +# external_libs = ["fileseq/src", "meshio/src", "python-future/src", "rich"] +# for lib in external_libs: +# lib_path = os.path.join(current_folder, "extern", lib) +# if lib_path not in sys.path: +# sys.path.append(lib_path) + -if bpy.context.preferences.filepaths.use_relative_paths == True: - bpy.context.preferences.filepaths.use_relative_paths = False +# if bpy.context.preferences.filepaths.use_relative_paths == True: +# bpy.context.preferences.filepaths.use_relative_paths = False -from bseq import * -from bseq.operators import menu_func_import, add_keymap, delete_keymap +from .bseq import * +from .bseq.operators import menu_func_import, add_keymap, delete_keymap classes = [ BSEQ_obj_property, @@ -57,7 +54,8 @@ # BSEQ_OT_import_zip, # BSEQ_OT_delete_zips, # BSEQ_addon_preferences, - BSEQ_OT_load_all + BSEQ_OT_load_all, + BSEQ_OT_load_all_recursive ] def register(): diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..92c19ec --- /dev/null +++ b/blender_manifest.toml @@ -0,0 +1,80 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "sequence_loader" +version = "0.3.4" +name = "Blender Sequence Loader" +tagline = "Just-in-time loader for meshio-supported mesh file sequences" +maintainer = "Stefan Rhys Jeske " +# Supported types: "add-on", "theme" +type = "add-on" + +# # Optional: link to documentation, support, source files, etc +website = "https://github.com/InteractiveComputerGraphics/blender-sequence-loader" + +# # Optional: tag list defined by Blender and server, see: +# # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html +tags = ["Animation", "Object"] + +blender_version_min = "4.2.0" +# # Optional: Blender version that the extension does not support, earlier versions are supported. +# # This can be omitted and defined later on the extensions platform if an issue is found. +# blender_version_max = "5.1.0" + +# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) +# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html +license = [ + "SPDX:MIT", +] +# # Optional: required by some licenses. +# copyright = [ +# "2002-2024 Developer Name", +# "1998 Company Name", +# ] + +# # Optional: list of supported platforms. If omitted, the extension will be available in all operating systems. +# platforms = ["windows-x64", "macos-arm64", "linux-x64"] +# # Other supported platforms: "windows-arm64", "macos-x64" + +# # Optional: bundle 3rd party Python modules. +# # https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html +wheels = [ + "./wheels/Fileseq-1.15.2-py3-none-any.whl", + "./wheels/future-0.18.3-py3-none-any.whl", + "./wheels/meshio-5.3.4-py3-none-any.whl", + "./wheels/rich-13.7.0-py3-none-any.whl", +] + +# # Optional: add-ons can list which resources they will require: +# # * files (for access of any filesystem operations) +# # * network (for internet access) +# # * clipboard (to read and/or write the system clipboard) +# # * camera (to capture photos and videos) +# # * microphone (to capture audio) +# # +# # If using network, remember to also check `bpy.app.online_access` +# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access +# # +# # For each permission it is important to also specify the reason why it is required. +# # Keep this a single short sentence without a period (.) at the end. +# # For longer explanations use the documentation or detail page. +# +[permissions] +files = "Core functionality to load files from disk" + +# # Optional: advanced build settings. +# # https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build +[build] +# These are the default build excluded patterns. +# You only need to edit them if you want different options. +paths_exclude_pattern = [ + "__pycache__/", + "/.git/", + "/*.zip", + "/extern/", + "/docs/", + "/images/", + "build_addon.py", + "download_wheels.sh" +] \ No newline at end of file diff --git a/bseq/__init__.py b/bseq/__init__.py index 114296c..13ca993 100644 --- a/bseq/__init__.py +++ b/bseq/__init__.py @@ -1,13 +1,14 @@ -from bseq.utils import refresh_obj -from .operators import BSEQ_OT_load, BSEQ_OT_edit, BSEQ_OT_resetpt, BSEQ_OT_resetmesh, BSEQ_OT_resetins, BSEQ_OT_set_as_split_norm, BSEQ_OT_remove_split_norm, BSEQ_OT_disable_selected, BSEQ_OT_enable_selected, BSEQ_OT_refresh_seq, BSEQ_OT_disable_all, BSEQ_OT_enable_all, BSEQ_OT_refresh_sequences, BSEQ_OT_set_start_end_frames, BSEQ_OT_batch_sequences, BSEQ_PT_batch_sequences_settings, BSEQ_OT_meshio_object, BSEQ_OT_import_zip, BSEQ_OT_delete_zips, BSEQ_addon_preferences, BSEQ_OT_load_all +from .utils import refresh_obj +from .operators import BSEQ_OT_load, BSEQ_OT_edit, BSEQ_OT_resetpt, BSEQ_OT_resetmesh, BSEQ_OT_resetins, BSEQ_OT_set_as_split_norm, BSEQ_OT_remove_split_norm, BSEQ_OT_disable_selected, BSEQ_OT_enable_selected, BSEQ_OT_refresh_seq, BSEQ_OT_disable_all, BSEQ_OT_enable_all, BSEQ_OT_refresh_sequences, BSEQ_OT_set_start_end_frames, BSEQ_OT_batch_sequences, BSEQ_PT_batch_sequences_settings, BSEQ_OT_meshio_object, BSEQ_OT_import_zip, BSEQ_OT_delete_zips, BSEQ_addon_preferences, BSEQ_OT_load_all, BSEQ_OT_load_all_recursive from .properties import BSEQ_scene_property, BSEQ_obj_property, BSEQ_mesh_property from .panels import BSEQ_UL_Obj_List, BSEQ_List_Panel, BSEQ_Settings, BSEQ_PT_Import, BSEQ_PT_Import_Child1, BSEQ_PT_Import_Child2, BSEQ_Globals_Panel, BSEQ_Advanced_Panel, BSEQ_Templates, BSEQ_UL_Att_List, draw_template from .messenger import subscribe_to_selected, unsubscribe_to_selected -import bpy -from bpy.app.handlers import persistent from .importer import update_obj from .globals import * +import bpy +from bpy.app.handlers import persistent + @persistent def BSEQ_initialize(scene): @@ -62,5 +63,6 @@ def BSEQ_initialize(scene): "BSEQ_OT_import_zip", "BSEQ_OT_delete_zips", "BSEQ_addon_preferences", - "BSEQ_OT_load_all" + "BSEQ_OT_load_all", + "BSEQ_OT_load_all_recursive" ] diff --git a/additional_file_formats/README.md b/bseq/additional_file_formats/README.md similarity index 100% rename from additional_file_formats/README.md rename to bseq/additional_file_formats/README.md diff --git a/additional_file_formats/__init__.py b/bseq/additional_file_formats/__init__.py similarity index 100% rename from additional_file_formats/__init__.py rename to bseq/additional_file_formats/__init__.py diff --git a/additional_file_formats/bgeo.py b/bseq/additional_file_formats/bgeo.py similarity index 100% rename from additional_file_formats/bgeo.py rename to bseq/additional_file_formats/bgeo.py diff --git a/additional_file_formats/mzd.py b/bseq/additional_file_formats/mzd.py similarity index 100% rename from additional_file_formats/mzd.py rename to bseq/additional_file_formats/mzd.py diff --git a/additional_file_formats/obj.py b/bseq/additional_file_formats/obj.py similarity index 100% rename from additional_file_formats/obj.py rename to bseq/additional_file_formats/obj.py diff --git a/additional_file_formats/table.py b/bseq/additional_file_formats/table.py similarity index 100% rename from additional_file_formats/table.py rename to bseq/additional_file_formats/table.py diff --git a/bseq/callback.py b/bseq/callback.py index 6aaff62..079b702 100644 --- a/bseq/callback.py +++ b/bseq/callback.py @@ -1,30 +1,36 @@ import bpy import fileseq +import traceback + +from .utils import show_message_box # Code here are mostly about the callback/update/items functions used in properties.py file_sequences = [] def update_path(self, context): + ''' + Detects all the file sequences in the directory + ''' + # When the path has been changed, reset the selected sequence to None context.scene.BSEQ['fileseq'] = 1 context.scene.BSEQ.use_pattern = False context.scene.BSEQ.pattern = "" - - ''' - Detects all the file sequences in the directory - ''' + file_sequences.clear() p = context.scene.BSEQ.path try: - f = fileseq.findSequencesOnDisk(p) - except: - return [("None", "No sequence detected", "", 1)] + f = fileseq.findSequencesOnDisk(bpy.path.abspath(p)) + except Exception as e: + show_message_box("Error when reading path\n" + traceback.format_exc(), + "fileseq Error" + str(e), + icon="ERROR") + return None if not f: - return [("None", "No sequence detected", "", 1)] + return None - file_sequences.clear() if len(f) >= 30: file_sequences.append(("None", "Too much sequence detected, could be false detection, please use pattern below", "", 1)) else: diff --git a/bseq/importer.py b/bseq/importer.py index 6e2e5cd..523b6f7 100644 --- a/bseq/importer.py +++ b/bseq/importer.py @@ -4,13 +4,17 @@ import traceback import fileseq import os -from .utils import show_message_box, get_relative_path, get_absolute_path +from .utils import show_message_box, get_relative_path, get_absolute_path, load_meshio_from_path import numpy as np from mathutils import Matrix import time # this import is not useless -import additional_file_formats +from .additional_file_formats import * +def extract_edges(cell: meshio.CellBlock): + if cell.type == "line": + return cell.data.astype(np.uint64) + return np.array([]) def extract_faces(cell: meshio.CellBlock): if cell.type == "triangle": @@ -51,6 +55,8 @@ def extract_faces(cell: meshio.CellBlock): return faces elif cell.type == "vertex": return np.array([]) + elif cell.type == "line": + return np.array([]) show_message_box(cell.type + " is unsupported mesh format yet") return np.array([]) @@ -98,7 +104,7 @@ def create_or_retrieve_attribute(mesh, k, v): if len(v.shape) == 2: dim = v.shape[1] if dim > 3: - show_message_box('higher than 3 dimensional attribue, ignored') + # show_message_box('higher than 3 dimensional attribue, ignored') return None if dim == 1: return mesh.attributes.new(k, "FLOAT", "POINT") @@ -107,7 +113,7 @@ def create_or_retrieve_attribute(mesh, k, v): if dim == 3: return mesh.attributes.new(k, "FLOAT_VECTOR", "POINT") if len(v.shape) > 2: - show_message_box('more than 2 dimensional tensor, ignored') + # show_message_box('more than 2 dimensional tensor, ignored') return None else: return mesh.attributes[k] @@ -124,21 +130,27 @@ def update_mesh(meshio_mesh, mesh): mesh.update() mesh.validate() return + edges = np.array([], dtype=np.uint64) faces_loop_start = np.array([], dtype=np.uint64) faces_loop_total = np.array([], dtype=np.uint64) loops_vert_idx = np.array([], dtype=np.uint64) shade_scheme = False if mesh.polygons: shade_scheme = mesh.polygons[0].use_smooth + for cell in meshio_mesh.cells: - data = extract_faces(cell) - # np array can't be simply written as `if not data:`, - if not data.any(): - continue - n_poly += len(data) - n_loop += data.shape[0] * data.shape[1] - loops_vert_idx = np.append(loops_vert_idx, data.ravel()) - faces_loop_total = np.append(faces_loop_total, np.ones((len(data)), dtype=np.uint64) * data.shape[1]) + edge_data = extract_edges(cell) + face_data = extract_faces(cell) + + if edge_data.any(): + edges = np.append(edges, edge_data) + + if face_data.any(): + n_poly += len(face_data) + n_loop += face_data.shape[0] * face_data.shape[1] + loops_vert_idx = np.append(loops_vert_idx, face_data.ravel()) + faces_loop_total = np.append(faces_loop_total, np.ones((len(face_data)), dtype=np.uint64) * face_data.shape[1]) + if faces_loop_total.size > 0: faces_loop_start = np.cumsum(faces_loop_total) # Add a zero as first entry @@ -150,10 +162,12 @@ def update_mesh(meshio_mesh, mesh): else: mesh.clear_geometry() mesh.vertices.add(n_verts) + mesh.edges.add(len(edges)) mesh.loops.add(n_loop) mesh.polygons.add(n_poly) mesh.vertices.foreach_set("co", mesh_vertices.ravel()) + mesh.edges.foreach_set("vertices", edges) mesh.loops.foreach_set("vertex_index", loops_vert_idx) mesh.polygons.foreach_set("loop_start", faces_loop_start) mesh.polygons.foreach_set("loop_total", faces_loop_total) @@ -161,7 +175,7 @@ def update_mesh(meshio_mesh, mesh): # newer function but is about 4 times slower # mesh.clear_geometry() - # mesh.from_pydata(mesh_vertices, [], data) + # mesh.from_pydata(mesh_vertices, edge_data, face_data) mesh.update() mesh.validate() @@ -178,6 +192,8 @@ def update_mesh(meshio_mesh, mesh): for k, v in meshio_mesh.point_data.items(): k = "bseq_" + k attribute = create_or_retrieve_attribute(mesh, k, v) + if attribute is None: + continue name_string = None if attribute.data_type == "FLOAT": name_string = "value" @@ -188,7 +204,11 @@ def update_mesh(meshio_mesh, mesh): # set as split normal per vertex if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == k: - mesh.use_auto_smooth = True + # If blender version is greater than 4.1.0, then don't set auto smooth. + # It has been removed and normals will be used automatically if they are set. + # https://developer.blender.org/docs/release_notes/4.1/python_api/#mesh + if bpy.app.version < (4, 1, 0): + mesh.use_auto_smooth = True mesh.normals_split_custom_set_from_vertices(v) for k, v in meshio_mesh.field_data.items(): @@ -197,12 +217,13 @@ def update_mesh(meshio_mesh, mesh): # set split normal per loop per vertex if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == k: - # Currently hard-coded for .obj files - mesh.use_auto_smooth = True + if bpy.app.version < (4, 1, 0): + mesh.use_auto_smooth = True + # currently hard-coded for .obj files indices = [item for sublist in meshio_mesh.cell_data["obj:vn_face_idx"][0] for item in sublist] mesh.normals_split_custom_set([meshio_mesh.field_data["obj:vn"][i - 1] for i in indices]) -# function to create a single meshio object +# function to create a single meshio object (not a sequence, this just inports some file using meshio) def create_meshio_obj(filepath): meshio_mesh = None try: @@ -241,13 +262,14 @@ def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix.Identit object = bpy.data.objects.new(name, mesh) # create the object + full_path = str(fileseq) + path = os.path.dirname(full_path) + pattern = os.path.basename(full_path) if use_relative: - full_path = get_relative_path(str(fileseq), root_path) - else: - full_path = str(fileseq) + path = get_relative_path(path, root_path) # path is only the directory in which the file is located - object.BSEQ.path = os.path.dirname(full_path) - object.BSEQ.pattern = os.path.basename(full_path) + object.BSEQ.path = path + object.BSEQ.pattern = pattern object.BSEQ.current_file = filepath object.BSEQ.init = True object.BSEQ.enabled = enabled @@ -316,16 +338,18 @@ def update_obj(scene, depsgraph=None): finally: del locals()['preprocess'] else: - filepath = fs[current_frame % len(fs)] - filepath = os.path.normpath(filepath) - try: - meshio_mesh = meshio.read(filepath) - obj.BSEQ.current_file = filepath - except Exception as e: - show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), - "Meshio Loading Error" + str(e), - icon="ERROR") - continue + if obj.BSEQ.match_frames: + fs_frames = fs.frameSet() + if current_frame in fs_frames: + filepath = fs[fs_frames.index(current_frame)] + filepath = os.path.normpath(filepath) + meshio_mesh = load_meshio_from_path(fs, filepath, obj) + else: + meshio_mesh = meshio.Mesh([], []) + else: + filepath = fs[current_frame % len(fs)] + filepath = os.path.normpath(filepath) + meshio_mesh = load_meshio_from_path(fs, filepath, obj) if not isinstance(meshio_mesh, meshio.Mesh): show_message_box('function preprocess does not return meshio object', "ERROR") diff --git a/bseq/messenger.py b/bseq/messenger.py index 0409779..25cceab 100644 --- a/bseq/messenger.py +++ b/bseq/messenger.py @@ -20,7 +20,8 @@ def selected_callback(): bpy.context.scene.BSEQ.edit_obj = bpy.context.active_object def subscribe_to_selected(): - import bseq + # import bseq + bseq = __loader__ # because current implementation may subscribe twice # so clear once to avoid duplication @@ -37,5 +38,6 @@ def subscribe_to_selected(): def unsubscribe_to_selected(): - import bseq + # import bseq + bseq = __loader__ bpy.msgbus.clear_by_owner(bseq) diff --git a/bseq/operators.py b/bseq/operators.py index 20e308e..7168868 100644 --- a/bseq/operators.py +++ b/bseq/operators.py @@ -6,6 +6,7 @@ from .utils import refresh_obj, show_message_box, get_relative_path from .importer import create_obj, create_meshio_obj import numpy as np +import os addon_name = "blendersequenceloader" @@ -22,9 +23,9 @@ def get_transform_matrix(importer_prop): def create_obj_wrapper(seq, importer_prop): create_obj(seq, importer_prop.use_relative, importer_prop.root_path, transform_matrix=get_transform_matrix(importer_prop)) -# Here are load and delete operations +# Legacy import operator (this is what the "Import from folder" button does) class BSEQ_OT_load(bpy.types.Operator): - '''This operator loads a sequence''' + '''Load selected sequence''' bl_label = "Load Sequence" bl_idname = "sequence.load" bl_options = {"UNDO"} @@ -49,7 +50,8 @@ def execute(self, context): fs = importer_prop.path + '/' + importer_prop.pattern try: - fs = fileseq.findSequenceOnDisk(fs) + # Call os.path.abspath in addition because findSequenceOnDisk does not support \..\ components on Windows apparently + fs = fileseq.findSequenceOnDisk(os.path.abspath(bpy.path.abspath(fs))) except Exception as e: show_message_box(traceback.format_exc(), "Can't find sequence: " + str(fs), "ERROR") return {"CANCELLED"} @@ -59,8 +61,8 @@ def execute(self, context): class BSEQ_OT_edit(bpy.types.Operator): - '''This operator changes a sequence''' - bl_label = "Edit Sequence's Path" + '''Edit Sequence''' + bl_label = "Edit the path of the sequence" bl_idname = "sequence.edit" bl_options = {"UNDO"} @@ -217,7 +219,7 @@ def execute(self, context): class BSEQ_OT_set_as_split_norm(bpy.types.Operator): - '''This operator sets the vertex attributes as vertex split normals''' + '''Set vertex attribute as vertex split normals''' bl_label = "Set as split normal per vertex" bl_idname = "bseq.setsplitnorm" bl_options = {"UNDO"} @@ -236,7 +238,7 @@ def execute(self, context): class BSEQ_OT_remove_split_norm(bpy.types.Operator): - '''This operator removes the vertex attributes as vertex split normals''' + '''Remove vertex attribute as vertex split normals''' bl_label = "Remove split normal per vertex" bl_idname = "bseq.removesplitnorm" bl_options = {"UNDO"} @@ -252,8 +254,8 @@ def execute(self, context): class BSEQ_OT_disable_selected(bpy.types.Operator): - '''This operator disables all selected sequence''' - bl_label = "Disable selected sequence" + '''Deactivate selected sequences''' + bl_label = "Deactivate sequence" bl_idname = "bseq.disableselected" bl_options = {"UNDO"} @@ -265,8 +267,8 @@ def execute(self, context): class BSEQ_OT_enable_selected(bpy.types.Operator): - '''This operator enables all selected sequence''' - bl_label = "Enable selected sequence" + '''Activate selected sequences''' + bl_label = "Activate sequence" bl_idname = "bseq.enableselected" bl_options = {"UNDO"} @@ -278,7 +280,7 @@ def execute(self, context): class BSEQ_OT_refresh_seq(bpy.types.Operator): - '''This operator refreshes the sequence''' + '''Refresh selected sequences''' bl_label = "Refresh sequence" bl_idname = "bseq.refresh" @@ -290,8 +292,8 @@ def execute(self, context): return {"FINISHED"} class BSEQ_OT_disable_all(bpy.types.Operator): - '''This operator disables all selected sequence''' - bl_label = "Disable all sequences" + '''Deactivate all sequences''' + bl_label = "Deactivate all sequences" bl_idname = "bseq.disableall" bl_options = {"UNDO"} @@ -302,8 +304,8 @@ def execute(self, context): return {"FINISHED"} class BSEQ_OT_enable_all(bpy.types.Operator): - '''This operator enables all selected sequence''' - bl_label = "Enable all sequences" + '''Activate all sequences''' + bl_label = "Activate all sequences" bl_idname = "bseq.enableall" bl_options = {"UNDO"} @@ -314,8 +316,8 @@ def execute(self, context): return {"FINISHED"} class BSEQ_OT_refresh_sequences(bpy.types.Operator): - '''This operator refreshes all found sequences''' - bl_label = "Refresh all sequences" + '''Refresh all sequences''' + bl_label = "Reloads everything in selected folder" bl_idname = "bseq.refreshall" bl_options = {"UNDO"} @@ -345,8 +347,9 @@ def execute(self, context): import meshio from bpy_extras.io_utils import ImportHelper +# This is what the button "Import Sequences" does class BSEQ_OT_batch_sequences(bpy.types.Operator, ImportHelper): - """Batch Import Sequences""" + """Import one or multiple sequences""" bl_idname = "wm.seq_import_batch" bl_label = "Import Sequences" bl_options = {'PRESET', 'UNDO'} @@ -438,7 +441,7 @@ def draw(self, context): # layout.prop(importer_prop, "root_path", text="Root Directory") class BSEQ_addon_preferences(bpy.types.AddonPreferences): - bl_idname = addon_name + bl_idname = __package__ zips_folder: bpy.props.StringProperty( name="Zips Folder", @@ -520,7 +523,7 @@ def execute(self, context): return {'FINISHED'} class BSEQ_OT_load_all(bpy.types.Operator): - """Load all sequences""" + """Load all sequences from selected folder and its subfolders""" bl_idname = "bseq.load_all" bl_label = "Load All" bl_options = {'PRESET', 'UNDO'} @@ -531,8 +534,8 @@ def execute(self, context): if importer_prop.use_relative and not bpy.data.is_saved: return relative_path_error() - dir = importer_prop.path - seqs = fileseq.findSequencesOnDisk(str(dir)) + p = importer_prop.path + seqs = fileseq.findSequencesOnDisk(bpy.path.abspath(p)) for s in seqs: print(s) @@ -540,6 +543,74 @@ def execute(self, context): for s in seqs: create_obj_wrapper(s, importer_prop) return {'FINISHED'} + +class BSEQ_OT_load_all_recursive(bpy.types.Operator): + """Load all sequences from selected folder recursively""" + bl_idname = "bseq.load_all_recursive" + bl_label = "Load All Recursive" + bl_options = {'PRESET', 'UNDO'} + + def execute(self, context): + importer_prop = context.scene.BSEQ + + if importer_prop.use_relative and not bpy.data.is_saved: + return relative_path_error() + + root_dir = bpy.path.abspath(importer_prop.path) + root_coll = bpy.context.scene.collection + root_layer_collection = bpy.context.view_layer.layer_collection + unlinked_collections = [] + # Recurse through directory itself and subdirectories + for current_dir, subdirs, files in os.walk(root_dir): + seqs = fileseq.findSequencesOnDisk(current_dir) + if len(seqs) == 0: + continue + + # Get list of directories from the root_dir to the current directory + coll_list = bpy.path.relpath(current_dir, start=root_dir).strip("//").split("/") + + # Get or create a nested collection starting from the root + last_coll = root_coll + layer_collection = root_layer_collection + for coll in coll_list: + # If it already exists and is not in the children of the last collection, then the prefix has changed + cur_coll = bpy.data.collections.get(coll) + if cur_coll is not None and last_coll is not None: + if cur_coll.name not in last_coll.children: + # Get the old parent of the existing collection and move the children to the old parent + parent = [c for c in bpy.data.collections if bpy.context.scene.user_of_id(cur_coll) and cur_coll.name in c.children] + if len(parent) > 0: + for child in cur_coll.children: + parent[0].children.link(child) + for obj in cur_coll.objects: + parent[0].objects.link(obj) + parent[0].children.unlink(cur_coll) + unlinked_collections.append(cur_coll) + else: + layer_collection = layer_collection.children[cur_coll.name] + last_coll = cur_coll + + + # If it was newly created, link it to the last collection + if cur_coll is None and last_coll is not None: + cur_coll = bpy.data.collections.new(coll) + last_coll.children.link(cur_coll) + layer_collection = layer_collection.children[cur_coll.name] + last_coll = cur_coll + + # Set the last collection as the active collection by recursing through the collections + context.view_layer.active_layer_collection = layer_collection + + # for s in seqs: + # print(s) + + for s in seqs: + create_obj_wrapper(s, importer_prop) + + # Make sure unused datablocks are freed + for coll in unlinked_collections: + bpy.data.collections.remove(coll) + return {'FINISHED'} class BSEQ_OT_meshio_object(bpy.types.Operator, ImportHelper): diff --git a/bseq/panels.py b/bseq/panels.py index 6fdd6b1..5c9484e 100644 --- a/bseq/panels.py +++ b/bseq/panels.py @@ -184,6 +184,9 @@ def draw(self, context): col1.alignment = 'RIGHT' col2 = split.column(align=False) + col1.label(text='Match Blender frame numbers') + col2.prop(obj.BSEQ, 'match_frames', text="") + col1.label(text='Path') col2.prop(obj.BSEQ, 'path', text="") col1.label(text='Pattern') @@ -235,10 +238,15 @@ def draw(self, context): # col2.prop(importer_prop, "filter_string", text="Filter String") - col1.label(text="Relative Path") + col1.label(text="Relative Paths") col2.prop(importer_prop, "use_relative", text="") - col1.label(text="Import Default Normals") + if importer_prop.use_relative: + col1.label(text="Relative Root") + col2.prop(importer_prop, "root_path", text="") + + + col1.label(text="Import Normals") col2.prop(importer_prop, "use_imported_normals", text="") col1.label(text="Custom Transform") @@ -289,11 +297,13 @@ def draw(self, context): col3.prop(importer_prop, "fileseq", text="") col4.operator("bseq.refreshall", text='', icon="FILE_REFRESH") - split = layout.split(factor=0.7) + split = layout.split(factor=0.5) col1 = split.column() col2 = split.column() col1.operator("sequence.load") - col2.operator("bseq.load_all") + row = col2.row() + row.operator("bseq.load_all") + row.operator("bseq.load_all_recursive") # split = layout.split(factor=0.5) # col1 = split.column() diff --git a/bseq/properties.py b/bseq/properties.py index cbaec85..c99903e 100644 --- a/bseq/properties.py +++ b/bseq/properties.py @@ -9,51 +9,51 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): update=update_path, ) - use_relative: bpy.props.BoolProperty(name='Use relative path', - description="Use relative path", + use_relative: bpy.props.BoolProperty(name='Relative Paths', + description="Toggle relative paths on/off", default=False, ) - use_imported_normals: bpy.props.BoolProperty(name='Use Imported Normals', - description="Use normals from imported mesh", + use_imported_normals: bpy.props.BoolProperty(name='Import Normals', + description="Use normals from imported mesh (see README for details)", default=False, ) root_path: bpy.props.StringProperty(name="Root Directory", subtype="DIR_PATH", - description="Select a root folder for all relative paths. If not set, the current filename is used", + description="Select root folder for all relative paths. If empty, root is folder of the Blender file", update=update_path, default="", ) fileseq: bpy.props.EnumProperty( name="File Sequences", - description="Choose file sequences.", + description="Select a file sequence", items=item_fileseq, ) - use_pattern: bpy.props.BoolProperty(name='Use pattern', - description="Use manually typed pattern, if the sequence can't be deteced", + use_pattern: bpy.props.BoolProperty(name='Custom Pattern', + description="Use manually typed pattern. Useful if the sequence can't be deteced", default=False, ) pattern: bpy.props.StringProperty(name="Pattern", - description="Custom pattern.", + description="Custom pattern. Use @ for frame number. Example: file_@.obj", ) selected_obj_deselectall_flag: bpy.props.BoolProperty(default=True, description="Flag that determines whether to deselect all items or not", ) - selected_obj_num: bpy.props.IntProperty(name='imported count', - description='Number of imported sequences, when selecting from UI list', + selected_obj_num: bpy.props.IntProperty(name='Sequences List', default=0, update=update_selected_obj_num, ) - selected_attribute_num: bpy.props.IntProperty(default=0) + selected_attribute_num: bpy.props.IntProperty(name="Select Vertex Attribute",default=0) material: bpy.props.PointerProperty( + name="Material", type=bpy.types.Material, poll=poll_material, ) @@ -64,7 +64,7 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): ) print: bpy.props.BoolProperty(name='Print Sequence Information', - description="Print additional information during rendering to a file in the same folder as the render output", + description="Print useful information during rendering to file in same folder as render output", default=True, ) @@ -90,7 +90,7 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): ) custom_rotation: bpy.props.FloatVectorProperty(name='Custom Rotation', - description='Set custom rotation vector', + description='Set custom Euler angles', size=3, subtype="EULER", default=[0,0,0], @@ -116,14 +116,19 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): class BSEQ_obj_property(bpy.types.PropertyGroup): init: bpy.props.BoolProperty(default=False) enabled: bpy.props.BoolProperty(default=True, - description="If disabled, the sequence won't be updated each frame") + name="Activate/Deactivate", + description="If deactivated, sequence won't be updated each frame") use_advance: bpy.props.BoolProperty(default=False) - script_name: bpy.props.StringProperty() - path: bpy.props.StringProperty(subtype="DIR_PATH") - pattern: bpy.props.StringProperty() - current_file: bpy.props.StringProperty() - frame: bpy.props.IntProperty() + script_name: bpy.props.StringProperty(name="Script name") + path: bpy.props.StringProperty(name="Path of sequence", subtype="DIR_PATH") + pattern: bpy.props.StringProperty(name="Pattern of sequence") + current_file: bpy.props.StringProperty(description="File of sequence that is currently loaded") + frame: bpy.props.IntProperty(name="Frame") start_end_frame: bpy.props.IntVectorProperty(name="Start and end frames", size=2, default=(0, 0)) + match_frames: bpy.props.BoolProperty(default=False, + name="Match Blender frame numbers", + description="Show only frames that match the current frame number", + ) last_benchmark: bpy.props.FloatProperty(name="Last loading time") # set this property for mesh, not object (maybe change later?) diff --git a/bseq/utils.py b/bseq/utils.py index 4e7e957..161542c 100644 --- a/bseq/utils.py +++ b/bseq/utils.py @@ -1,6 +1,8 @@ import bpy import fileseq import os +import meshio +import traceback def show_message_box(message="", title="Message Box", icon="INFO"): ''' @@ -19,7 +21,6 @@ def draw(self, context): stop_animation() bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) - def stop_animation(): if bpy.context.screen.is_animation_playing: # if playing animation, then stop it, otherwise it will keep showing message box @@ -27,17 +28,18 @@ def stop_animation(): def get_relative_path(path, root_path): if root_path != "": - path = bpy.path.relpath(path, start=root_path) + rel_path = bpy.path.relpath(path, start=bpy.path.abspath(root_path)) else: - path = bpy.path.relpath(path) - return path + rel_path = bpy.path.relpath(path) + return rel_path # convert relative path to absolute path def convert_to_absolute_path(path, root_path): + # Additional call to os.path.abspath removes any "/../"" in the path (can be a problem on Windows) if root_path != "": - path = bpy.path.abspath(path, start=root_path) + path = os.path.abspath(bpy.path.abspath(path, start=bpy.path.abspath(root_path))) else: - path = bpy.path.abspath(path) + path = os.path.abspath(bpy.path.abspath(path)) return path def get_absolute_path(obj, scene): @@ -45,16 +47,31 @@ def get_absolute_path(obj, scene): full_path = convert_to_absolute_path(full_path, scene.BSEQ.root_path) return full_path - def refresh_obj(obj, scene): is_relative = obj.BSEQ.path.startswith("//") - print("is_relative: ", is_relative) fs = get_absolute_path(obj, scene) fs = fileseq.findSequenceOnDisk(fs) - fs = fileseq.findSequenceOnDisk(fs.dirname() + fs.basename() + "@" + fs.extension()) - obj.BSEQ.start_end_frame = (fs.start(), fs.end()) - fs = str(fs) + #fs = fileseq.findSequenceOnDisk(fs.dirname() + fs.basename() + "@" + fs.extension()) + + full_path = str(fs) + path = os.path.dirname(full_path) + pattern = os.path.basename(full_path) if is_relative: - fs = get_relative_path(fs, scene.BSEQ.root_path) - obj.BSEQ.path = os.path.dirname(fs) - obj.BSEQ.pattern = os.path.basename(fs) + path = get_relative_path(path, scene.BSEQ.root_path) + + obj.BSEQ.path = path + obj.BSEQ.pattern = pattern + obj.BSEQ.start_end_frame = (fs.start(), fs.end()) + +def load_meshio_from_path(fileseq, filepath, obj = None): + try: + meshio_mesh = meshio.read(filepath) + if obj is not None: + obj.BSEQ.current_file = filepath + except Exception as e: + show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), + "Meshio Loading Error" + str(e), + icon="ERROR") + meshio_mesh = meshio.Mesh([], []) + return meshio_mesh + diff --git a/docs/conf.py b/docs/conf.py index 617f075..d4e3eb7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,9 +7,9 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'blender-sequence-loader' -copyright = '2022, InteractiveComputerGraphics' +copyright = '2025, InteractiveComputerGraphics' author = 'InteractiveComputerGraphics' -release = '0.1.5' +release = '0.3.4' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/download_wheels.sh b/download_wheels.sh new file mode 100755 index 0000000..1abd6b0 --- /dev/null +++ b/download_wheels.sh @@ -0,0 +1,4 @@ +pip wheel fileseq==1.15.2 -w ./wheels --no-deps +pip wheel meshio==5.3.4 -w ./wheels --no-deps +pip wheel future==0.18.3 -w ./wheels --no-deps +pip wheel rich==13.7.0 -w ./wheels --no-deps \ No newline at end of file diff --git a/images/drag.png b/images/drag.png index 9277da2..39fae42 100644 Binary files a/images/drag.png and b/images/drag.png differ diff --git a/images/list.png b/images/list.png index 09f119c..9e37f7f 100644 Binary files a/images/list.png and b/images/list.png differ diff --git a/images/location.png b/images/location.png index 074a05a..8b148ab 100644 Binary files a/images/location.png and b/images/location.png differ diff --git a/images/sequence.png b/images/sequence.png index a32bdcd..173eca0 100644 Binary files a/images/sequence.png and b/images/sequence.png differ diff --git a/template/Comparison Render.py b/template/Comparison Render.py new file mode 100644 index 0000000..f8b3e57 --- /dev/null +++ b/template/Comparison Render.py @@ -0,0 +1,64 @@ +import bpy + +# Utilities for comparison rendering +def toggle_on_single(obj): + obj.hide_render = False + if isinstance(obj, bpy.types.Object) and obj.BSEQ.init: + obj.BSEQ.enabled = True + for child in obj.children: + toggle_on_single(child) + elif isinstance(obj, bpy.types.Collection): + for child in obj.objects: + toggle_on_single(child) + for child in obj.children: + toggle_on_single(child) + +def toggle_on(objs): + if type(objs) == list: + for obj in objs: + toggle_on_single(obj) + else: + toggle_on_single(objs) + +def toggle_off_single(obj): + obj.hide_render = True + if isinstance(obj, bpy.types.Object) and obj.BSEQ.init: + obj.BSEQ.enabled = False + for child in obj.children: + toggle_off_single(child) + elif isinstance(obj, bpy.types.Collection): + for child in obj.objects: + toggle_off_single(child) + for child in obj.children: + toggle_off_single(child) + +def toggle_off(objs): + if type(objs) == list: + for obj in objs: + toggle_off_single(obj) + else: + toggle_off_single(objs) + +def toggle_off_all(): + for obj in bpy.data.objects: + toggle_off_single(obj) + +def toggle_on_all(): + for obj in bpy.data.objects: + toggle_on_single(obj) + +# Declare which collection to render comparison for +# Change this to the name of the collection you want to render +comparison_collection = "Sequences" + +# Iterate over children in the collection +comparison_objects = list(bpy.data.collections[comparison_collection].children) + list(bpy.data.collections[comparison_collection].objects) +orig_path = bpy.context.scene.render.filepath +for obj in comparison_objects: + toggle_off(comparison_objects) + toggle_on(obj) + bpy.context.scene.render.filepath = f"{orig_path}/{obj.name}/" +# bpy.ops.render.render(write_still=True) + bpy.ops.render.render(animation=True) + +bpy.context.scene.render.filepath = orig_path \ No newline at end of file