diff --git a/.gitignore b/.gitignore index 804a0ab..fe9c7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ __pycache__/ .DS_Store -.vs_code/ +.vscode/ *.zip +*.whl # the following ignores are used to ignore the local softlink files # the extern folder won't be affected by this rich meshio future -fileseq \ No newline at end of file +fileseq + +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 ae7498f..d2f2e77 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,77 @@ -![](images/logo.svg) - -Loading animation sequences with meshio and fileseq +![](images/logo_as_path.svg) + +![GitHub release (latest by date)](https://img.shields.io/github/v/release/InteractiveComputerGraphics/blender-sequence-loader) +[![Documentation Status](https://readthedocs.org/projects/blender-sequence-loader/badge/?version=latest)](https://blender-sequence-loader.readthedocs.io/en/latest/?badge=latest) + +This is an addon for Blender 4.0+ (might work with 2.8+ but is not extensively tested on less recent versions) that enables loading of file sequences. The addon comes bundled together with [meshio](https://github.com/nschloe/meshio) which enables the loading of geometric data from a multitude of file formats. As stated there, the supported formats are listed in the following. Note that not all of the formats have been tested and some issues may still occur. + +> [Abaqus](http://abaqus.software.polimi.it/v6.14/index.html) (`.inp`), +> ANSYS msh (`.msh`), +> [AVS-UCD](https://lanl.github.io/LaGriT/pages/docs/read_avs.html) (`.avs`), +> [CGNS](https://cgns.github.io/) (`.cgns`), +> [DOLFIN XML](https://manpages.ubuntu.com/manpages/jammy/en/man1/dolfin-convert.1.html) (`.xml`), +> [Exodus](https://nschloe.github.io/meshio/exodus.pdf) (`.e`, `.exo`), +> [FLAC3D](https://www.itascacg.com/software/flac3d) (`.f3grid`), +> [H5M](https://www.mcs.anl.gov/~fathom/moab-docs/h5mmain.html) (`.h5m`), +> [Kratos/MDPA](https://github.com/KratosMultiphysics/Kratos/wiki/Input-data) (`.mdpa`), +> [Medit](https://people.sc.fsu.edu/~jburkardt/data/medit/medit.html) (`.mesh`, `.meshb`), +> [MED/Salome](https://docs.salome-platform.org/latest/dev/MEDCoupling/developer/med-file.html) (`.med`), +> [Nastran](https://help.autodesk.com/view/NSTRN/2019/ENU/?guid=GUID-42B54ACB-FBE3-47CA-B8FE-475E7AD91A00) (bulk data, `.bdf`, `.fem`, `.nas`), +> [Netgen](https://github.com/ngsolve/netgen) (`.vol`, `.vol.gz`), +> [Neuroglancer precomputed format](https://github.com/google/neuroglancer/tree/master/src/neuroglancer/datasource/precomputed#mesh-representation-of-segmented-object-surfaces), +> [Gmsh](https://gmsh.info/doc/texinfo/gmsh.html#File-formats) (format versions 2.2, 4.0, and 4.1, `.msh`), +> [OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) (`.obj`), +> [OFF](https://segeval.cs.princeton.edu/public/off_format.html) (`.off`), +> [PERMAS](https://www.intes.de) (`.post`, `.post.gz`, `.dato`, `.dato.gz`), +> [PLY]() (`.ply`), +> [STL]() (`.stl`), +> [Tecplot .dat](http://paulbourke.net/dataformats/tp/), +> [TetGen .node/.ele](https://wias-berlin.de/software/tetgen/fformats.html), +> [SVG](https://www.w3.org/TR/SVG/) (2D output only) (`.svg`), +> [SU2](https://su2code.github.io/docs_v7/Mesh-File/) (`.su2`), +> [UGRID](https://www.simcenter.msstate.edu/software/documentation/ug_io/3d_grid_file_type_ugrid.html) (`.ugrid`), +> [VTK](https://vtk.org/wp-content/uploads/2015/04/file-formats.pdf) (`.vtk`), +> [VTU](https://vtk.org/Wiki/VTK_XML_Formats) (`.vtu`), +> [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) ([TIN](https://en.wikipedia.org/wiki/Triangulated_irregular_network)) (`.wkt`), +> [XDMF](https://xdmf.org/index.php/XDMF_Model_and_Format) (`.xdmf`, `.xmf`). + +[fileseq](https://github.com/justinfx/fileseq) is used to identify and load file sequences, while [rich](https://github.com/Textualize/rich) and [python-future](https://github.com/PythonCharmers/python-future) are included to satisfy unmet dependencies of the other packages. + +All data is loaded *just-in-time* when the Blender frame changes, in order to avoid excessive memory consumption. By default, the addon is able to load vertices, lines, triangles and quads. It is also able to automatically extract triangle and quad surface meshes from tetrahedral and hexahedral volume meshes. Scalar and vector attributes on vertices are also imported for visualization purposes. See the following documentation for a brief introduction. + +**DISCLAIMER: This project is still very much under development, so breaking changes may occur at any time!** - [1. Installation](#1-installation) - [1.1 Build from source (optional)](#11-build-from-source-optional) - [1.2 Install Addon](#12-install-addon) - [1.3 FAQs](#13-faqs) - [2. How to use](#2-how-to-use) - - [2. Load the animation sequence you want](#2-load-the-animation-sequence-you-want) - - [2.1 Absolute vs. Relative Paths](#21-absolute-vs-relative-paths) - - [2.2 Sequence List View](#22-sequence-list-view) - - [2.2.1 Enable/ Disable](#221-enable-disable) - - [2.2.1 Refresh Sequence](#221-refresh-sequence) - - [2.3 Settings](#23-settings) - - [2.3.1 Geometry Nodes](#231-geometry-nodes) - - [2.3.2 Path Information](#232-path-information) - - [2.3.3 Attributes Settings](#233-attributes-settings) - - [2.3.4 Split Norm per Vertex](#234-split-norm-per-vertex) + - [1. Load the animation sequence you want](#1-load-the-animation-sequence-you-want) + - [1.1 Relative Paths](#11-relative-paths) + - [1.2 Import Default Normals](#12-import-default-normals) + - [1.3 Custom Transformation Matrix](#13-custom-transformation-matrix) + - [1.4 Load sequences from folder (Legacy importer)](#14-load-sequences-from-folder-legacy-importer) + - [2. Global Settings](#2-global-settings) + - [2.1 Root Directory](#21-root-directory) + - [2.2 Print Sequence Information](#22-print-sequence-information) + - [2.3 Auto Refresh Active Sequences](#23-auto-refresh-active-sequences) + - [2.4 Auto Refresh All Sequences](#24-auto-refresh-all-sequences) + - [3. Sequence List View](#3-sequence-list-view) + - [3.1 Activate / Deactivate Sequences](#31-activate--deactivate-sequences) + - [3.2 Refresh Sequence](#32-refresh-sequence) + - [3.3 Activate / Deactivate All](#33-activate--deactivate-all) + - [3.4 Set Timeline](#34-set-timeline) + - [4. Sequence Properties](#4-sequence-properties) + - [4.1 Match Blender Frame Numbers](#41-match-blender-frame-numbers) + - [4.2 Path](#42-path) + - [4.3 Pattern](#43-pattern) + - [4.4 Current File](#44-current-file) + - [4.5 Last Loading Time](#45-last-loading-time) + - [4.6 Attributes Settings](#46-attributes-settings) + - [4.6.1 Split Norm per Vertex](#461-split-norm-per-vertex) + - [5. Advanced Settings](#5-advanced-settings) + - [5.1 Script](#51-script) + - [5.2 Geometry Nodes](#52-geometry-nodes) ## 1. Installation @@ -30,8 +85,9 @@ git clone https://github.com/InteractiveComputerGraphics/blender-sequence-loader 2. Build the installable `.zip` file by simply running the following command. This should produce a file called `blender_sequence_loader_{date}.zip`, where `{date}` is replaced with todays date. No other dependency other than standard python3 libraries are needed to build the addon. -```shell -python3 build_addon.py +```sh +./download_wheels.sh +blender --command extension build ``` ### 1.2 Install Addon @@ -46,90 +102,157 @@ 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) -![start](images/0.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) Then you can find it here. -![homepage](images/1.png) +![homepage](images/location.png) + +### 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". -### 2. Load the animation sequence you want +#### 1.3 Custom Transformation Matrix -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.** +When enabling this option, you can define a custom transformation matrix (using XYZ Euler Angles) that will be applied once when importing a sequence. -![selecticon](images/2.png) +#### 1.4 Load sequences from folder (Legacy importer) -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. +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. -![after_selecting](images/3.png) +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. -#### 2.1 Absolute vs. Relative Paths +The refresh button simply looks again for sequences in the selcted folder, in case there were any changes made. -There is a small checkbox about whether to use `relative paths` or not. +Then click the `Load` Sequence" button to load the selected sequence or the `Load All` button to load all found sequences. -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. +![sequence](images/sequence.png) -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. Global Settings -![relative_path](images/4.png) +#### 2.1 Root Directory -#### 2.2 Sequence List View +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. -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. +#### 2.2 Print Sequence Information -![settings](images/5.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. -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. +#### 2.3 Auto Refresh Active 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. +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. -![lock interface](images/6.png) +#### 2.4 Auto Refresh All Sequences -##### 2.2.1 Enable/ Disable +Like the above but with all 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. +### 3. Sequence List View -##### 2.2.1 Refresh Sequence +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. + +##### 3.1 Activate / Deactivate Sequences + +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. + +##### 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. + +#### 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 -![material](images/7.png) +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. -#### 2.3.2 Path Information +#### 4.4 Current File -This shows the path of the sequence for debugging purposes, however it's not editable. +This is read-only and shows the absolute path of the file that is currenlty loaded from the selected sequence. -#### 2.3.3 Attributes Settings +#### 4.5 Last Loading Time + +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. + +### 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!!!** + +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 f3be434..d98c67b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,76 +1,90 @@ -bl_info = { - "name": "Sequence Loader", - "description": "Loader for meshio supported mesh files/ simulation sequences", - "author": "Interactive Computer Graphics", - "version": (1, 0), - "blender": (3, 1, 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 simloader import * +from .bseq import * +from .bseq.operators import menu_func_import, add_keymap, delete_keymap classes = [ - SIMLOADER_obj_property, - SIMLOADER_scene_property, - SIMLOADER_mesh_property, - SIMLOADER_OT_load, - SIMLOADER_OT_edit, - SIMLOADER_OT_resetpt, - SIMLOADER_OT_resetins, - SIMLOADER_OT_resetmesh, - SIMLOADER_Import, - SIMLOADER_List_Panel, - SIMLOADER_UL_Obj_List, - SIMLOADER_Settings, - SIMLOADER_Templates, - SIMLOADER_UL_Att_List, - SIMLOADER_OT_set_as_split_norm, - SIMLOADER_OT_remove_split_norm, - SIMLOADER_OT_disable_selected, - SIMLOADER_OT_enable_selected, - SIMLOADER_OT_refresh_seq, + BSEQ_obj_property, + BSEQ_scene_property, + BSEQ_mesh_property, + BSEQ_OT_load, + BSEQ_OT_edit, + BSEQ_OT_resetpt, + BSEQ_OT_resetins, + BSEQ_OT_resetmesh, + BSEQ_PT_Import, + BSEQ_PT_Import_Child1, + BSEQ_PT_Import_Child2, + BSEQ_Globals_Panel, + BSEQ_List_Panel, + BSEQ_UL_Obj_List, + BSEQ_Settings, + BSEQ_Advanced_Panel, + BSEQ_Templates, + BSEQ_UL_Att_List, + 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 ] - def register(): - bpy.app.handlers.load_post.append(SIMLOADER_initialize) + bpy.app.handlers.load_post.append(BSEQ_initialize) for cls in classes: bpy.utils.register_class(cls) bpy.types.TEXT_MT_templates.append(draw_template) - bpy.types.Scene.SIMLOADER = bpy.props.PointerProperty(type=SIMLOADER_scene_property) - bpy.types.Object.SIMLOADER = bpy.props.PointerProperty(type=SIMLOADER_obj_property) - bpy.types.Mesh.SIMLOADER = bpy.props.PointerProperty(type=SIMLOADER_mesh_property) - + bpy.types.Scene.BSEQ = bpy.props.PointerProperty(type=BSEQ_scene_property) + bpy.types.Object.BSEQ = bpy.props.PointerProperty(type=BSEQ_obj_property) + bpy.types.Mesh.BSEQ = bpy.props.PointerProperty(type=BSEQ_mesh_property) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + add_keymap() # manually call this function once # so when addon being installed, it can run correctly # because scene is not used, so pass None into it - SIMLOADER_initialize(None) + BSEQ_initialize(None) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) bpy.types.TEXT_MT_templates.remove(draw_template) - del bpy.types.Scene.SIMLOADER - del bpy.types.Object.SIMLOADER - bpy.app.handlers.load_post.remove(SIMLOADER_initialize) + del bpy.types.Scene.BSEQ + del bpy.types.Object.BSEQ + bpy.app.handlers.load_post.remove(BSEQ_initialize) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + delete_keymap() unsubscribe_to_selected() - if __name__ == "__main__": - # unregister() register() diff --git a/additional_file_formats/__init__.py b/additional_file_formats/__init__.py deleted file mode 100644 index ef8048a..0000000 --- a/additional_file_formats/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .mzd import readMZD_to_bpymesh, readMZD_to_meshio -from .bgeo import readbgeo_to_meshio - -additional_format_loader = {'bgeo': readbgeo_to_meshio, 'mzd': readMZD_to_meshio} - -__all__ = [ - readMZD_to_bpymesh, readMZD_to_meshio, readbgeo_to_meshio, additional_format_loader -] diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..b54ebff --- /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.6" +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 new file mode 100644 index 0000000..08a8e37 --- /dev/null +++ b/bseq/__init__.py @@ -0,0 +1,70 @@ +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 +from .importer import update_obj +from .globals import * + +import bpy +from bpy.app.handlers import persistent + + +@persistent +def BSEQ_initialize(scene): + if update_obj not in bpy.app.handlers.frame_change_post: + # Insert at the beginning, so that it runs before other frame change handlers. + # The other handlers don't need to be in the first position. + bpy.app.handlers.frame_change_post.insert(0, update_obj) + if auto_refresh_active not in bpy.app.handlers.frame_change_post: + bpy.app.handlers.frame_change_post.append(auto_refresh_active) + if auto_refresh_all not in bpy.app.handlers.frame_change_post: + bpy.app.handlers.frame_change_post.append(auto_refresh_all) + if clean_unused_bseq_data not in bpy.app.handlers.save_pre: + bpy.app.handlers.save_pre.append(clean_unused_bseq_data) + subscribe_to_selected() + if print_information not in bpy.app.handlers.render_init: + bpy.app.handlers.render_init.append(print_information) + + +__all__ = [ + "BSEQ_OT_edit", + "BSEQ_OT_load", + "BSEQ_obj_property", + "BSEQ_initialize", + "BSEQ_PT_Import", + "BSEQ_PT_Import_Child1", + "BSEQ_PT_Import_Child2", + "BSEQ_Globals_Panel", + "BSEQ_List_Panel", + "BSEQ_UL_Obj_List", + "BSEQ_scene_property", + "BSEQ_Templates", + "BSEQ_Settings", + "BSEQ_Advanced_Panel", + "BSEQ_UL_Att_List", + "subscribe_to_selected", + "BSEQ_OT_resetpt", + "BSEQ_OT_resetmesh", + "BSEQ_OT_resetins", + "draw_template", + "unsubscribe_to_selected", + "BSEQ_OT_set_as_split_norm", + "BSEQ_mesh_property", + "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" +] diff --git a/bseq/additional_file_formats/README.md b/bseq/additional_file_formats/README.md new file mode 100644 index 0000000..244b649 --- /dev/null +++ b/bseq/additional_file_formats/README.md @@ -0,0 +1,16 @@ +Here you can find an example if you want to import customized file formats. + +1. create an `example.py` +2. implement a function as following +```python +def read_func(filepath): + # open the filepath linked file + # construct the meshio object + return meshio.Mesh +``` +3. call `meshio.register_format("name", [".extenstion"], read_func, {".extenstion": None})`in global environment +4. add `from . import example` in `__init__.py` + +You can check `bgeo.py` as an example, which reads the partiles-only [.bgeo](https://github.com/wdas/partio) file. + +For more details how to construct the `meshio.Mesh` object, you can check [here](https://github.com/nschloe/meshio/wiki/meshio-Mesh()-data-structure) for the details about the data strucutre, and [here](https://github.com/nschloe/meshio/wiki/Node-ordering-in-cells) more details about the vertex ordering. diff --git a/bseq/additional_file_formats/__init__.py b/bseq/additional_file_formats/__init__.py new file mode 100644 index 0000000..66f512f --- /dev/null +++ b/bseq/additional_file_formats/__init__.py @@ -0,0 +1,3 @@ +from . import bgeo +from . import mzd +from . import obj \ No newline at end of file diff --git a/additional_file_formats/bgeo.py b/bseq/additional_file_formats/bgeo.py similarity index 97% rename from additional_file_formats/bgeo.py rename to bseq/additional_file_formats/bgeo.py index b2c08fe..feb05c1 100644 --- a/additional_file_formats/bgeo.py +++ b/bseq/additional_file_formats/bgeo.py @@ -105,3 +105,6 @@ def readbgeo_to_meshio(filepath): raise Exception("file didn't end") return meshio.Mesh(position, [('vertex', [])], point_data=point_attributes) + +# no need for write function +meshio.register_format("bgeo", [".bgeo"], readbgeo_to_meshio, {".bgeo": None}) diff --git a/additional_file_formats/mzd.py b/bseq/additional_file_formats/mzd.py similarity index 98% rename from additional_file_formats/mzd.py rename to bseq/additional_file_formats/mzd.py index 5cb9d54..af4ff3c 100644 --- a/additional_file_formats/mzd.py +++ b/bseq/additional_file_formats/mzd.py @@ -22,6 +22,7 @@ def readMZD_to_meshio(filepath): with open(filepath, 'rb') as file: byte = file.read(24) + # check if mzd file is empty if byte != head: return -4 while 1: @@ -49,7 +50,7 @@ def readMZD_to_meshio(filepath): if out_numVertices < 0: return -127 if out_numVertices == 0: - break + return meshio.Mesh(points=np.array([]), cells={}) byte = file.read(12 * out_numVertices) out_vertPositions = np.frombuffer(byte, dtype=np.float32) @@ -159,6 +160,7 @@ def readMZD_to_meshio(filepath): pass return meshio.Mesh(out_vertPositions.reshape((out_numVertices, 3)), cells, point_data) + def readMZD_to_bpymesh(filepath, mesh): shade_scheme = False if mesh.polygons: @@ -308,4 +310,8 @@ def readMZD_to_bpymesh(filepath, mesh): else: # print(name) file.seek(size, 1) - pass \ No newline at end of file + pass + + +# no need for write function +meshio.register_format("mzd", [".mzd"], readMZD_to_meshio, {".mzd": None}) \ No newline at end of file diff --git a/bseq/additional_file_formats/obj.py b/bseq/additional_file_formats/obj.py new file mode 100644 index 0000000..57437b3 --- /dev/null +++ b/bseq/additional_file_formats/obj.py @@ -0,0 +1,135 @@ +""" +I/O for the Wavefront .obj file format, cf. +. +""" +import datetime + +import numpy as np + +# from .._files import open_file +# from .._helpers import register_format +# from .._mesh import CellBlock, Mesh + +import meshio + + +def read(filename): + with open(filename, "r") as f: + mesh = read_buffer(f) + return mesh + + +def read_buffer(f): + points = [] + vertex_normals = [] + texture_coords = [] + face_groups = [] + face_normals = [] + face_texture_coords = [] + face_group_ids = [] + face_group_id = -1 + while True: + line = f.readline() + + if not line: + # EOF + break + + strip = line.strip() + + if len(strip) == 0 or strip[0] == "#": + continue + + split = strip.split() + + if split[0] == "v": + points.append([float(item) for item in split[1:]]) + elif split[0] == "vn": + vertex_normals.append([float(item) for item in split[1:]]) + elif split[0] == "vt": + texture_coords.append([float(item) for item in split[1:]]) + elif split[0] == "s": + # "s 1" or "s off" controls smooth shading + pass + elif split[0] == "f": + # old: dat = [int(item.split("/")[0]) for item in split[1:]] + # A face in obj has one of the following formats: 1, 1/2, 1//3, 1/2/3 + # We want to support all formats now amd store the texture and normal indices in other arrays + face_indices = [] + face_texture_indices = [] + face_normal_indices = [] + + for item in split[1:]: + indices = item.split("/") + face_indices.append(int(indices[0])) + if len(indices) > 1 and indices[1] != "": + face_texture_indices.append(int(indices[1])) + if len(indices) > 2: + face_normal_indices.append(int(indices[2])) + + if len(face_groups) == 0 or ( + len(face_groups[-1]) > 0 and len(face_groups[-1][-1]) != len(face_indices) + ): + face_groups.append([]) + face_group_ids.append([]) + face_texture_coords.append([]) + face_normals.append([]) + face_groups[-1].append(face_indices) + face_group_ids[-1].append(face_group_id) + if face_texture_indices: + face_texture_coords[-1].append(face_texture_indices) + if face_normal_indices: + face_normals[-1].append(face_normal_indices) + elif split[0] == "g": + # new group + face_groups.append([]) + face_group_ids.append([]) + face_texture_coords.append([]) + face_normals.append([]) + face_group_id += 1 + else: + # who knows + pass + + # There may be empty groups, too. + # Remove them. + face_groups = [f for f in face_groups if len(f) > 0] + face_group_ids = [g for g in face_group_ids if len(g) > 0] + face_normals = [n for n in face_normals if len(n) > 0] + face_texture_coords = [t for t in face_texture_coords if len(t) > 0] + + # convert to numpy arrays and remove + points = np.array(points) + face_groups = [np.array(f) for f in face_groups] + texture_coords = [np.array(t) for t in texture_coords] + vertex_normals = [np.array(n) for n in vertex_normals] + point_data = {} + cell_data = {} + field_data = {} + + if face_texture_coords and len(texture_coords) == max([max(max(face)) for face in face_texture_coords]): + field_data["obj:vt"] = texture_coords + cell_data["obj:vt_face_idx"] = face_texture_coords + elif len(texture_coords) == len(points): + point_data["obj:vt"] = texture_coords + + if face_normals and len(vertex_normals) == max([max(max(face)) for face in face_normals]): + field_data["obj:vn"] = vertex_normals + cell_data["obj:vn_face_idx"] = face_normals + elif len(vertex_normals) == len(points): + point_data["obj:vn"] = vertex_normals + + cell_data["obj:group_ids"] = [] + cells = [] + for f, gid in zip(face_groups, face_group_ids): + if f.shape[1] == 3: + cells.append(meshio.CellBlock("triangle", f - 1)) + elif f.shape[1] == 4: + cells.append(meshio.CellBlock("quad", f - 1)) + else: + cells.append(meshio.CellBlock("polygon", f - 1)) + cell_data["obj:group_ids"].append(gid) + + return meshio.Mesh(points, cells, point_data=point_data, cell_data=cell_data, field_data=field_data) + +meshio.register_format("obj", [".obj"], read, {"obj": None}) 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 new file mode 100644 index 0000000..079b702 --- /dev/null +++ b/bseq/callback.py @@ -0,0 +1,63 @@ +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 = "" + file_sequences.clear() + + p = context.scene.BSEQ.path + try: + 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 + + if len(f) >= 30: + file_sequences.append(("None", "Too much sequence detected, could be false detection, please use pattern below", "", 1)) + else: + count = 1 + for seq in f: + file_sequences.append((str(seq), seq.basename() + "@" + seq.extension(), "", count)) + count += 1 + + +def item_fileseq(self, context): + return file_sequences + + +def update_selected_obj_num(self, context): + + # Here is when select sequences, then change the corresponding object to active object + index = context.scene.BSEQ.selected_obj_num + obj = bpy.data.objects[index] + + if context.scene.BSEQ.selected_obj_deselectall_flag: + bpy.ops.object.select_all(action="DESELECT") + obj.select_set(True) + context.view_layer.objects.active = obj + + +def poll_material(self, material): + return not material.is_grease_pencil + +def poll_edit_obj(self, object): + return object.BSEQ.init \ No newline at end of file diff --git a/bseq/globals.py b/bseq/globals.py new file mode 100644 index 0000000..3a7ef10 --- /dev/null +++ b/bseq/globals.py @@ -0,0 +1,64 @@ +# here are the implementations of global settings + +import bpy +from datetime import datetime +import os +from .utils import refresh_obj + +def print_information(scene): + if not bpy.context.scene.BSEQ.print: + return + now = datetime.now() + path = bpy.context.scene.render.filepath + path = bpy.path.abspath(path) + if not os.path.isdir(path): + # by default, path is '/tmp', and it does not exist on windows system + return + filepath = path + '/bseq_' + now.strftime("%Y-%m-%d_%H-%M") + with open(filepath, 'w') as file: + file.write("Render Time: {}\n".format(now.strftime("%Y-%m-%d_%H-%M"))) + file.write("bseq Objects in the scene:\n\n") + for obj in bpy.data.objects: + bseq_prop = obj.BSEQ + if bseq_prop.init: + file.write("Object name: {}\n".format(obj.name)) + file.write("Is it being animated: {}\n".format(bseq_prop.enabled)) + file.write("Filepath: {}\n".format(bseq_prop.path)) + file.write("Pattern: {}\n".format(bseq_prop.pattern)) + file.write("Current file: {}\n".format(bseq_prop.current_file)) + file.write("\n\n") + + +def auto_refresh_all(scene, depsgraph=None): + if not bpy.context.scene.BSEQ.auto_refresh_all: + return + for obj in bpy.data.objects: + if obj.BSEQ.init == False: + continue + if obj.BSEQ.enabled == False: + continue + if obj.mode != "OBJECT": + continue + refresh_obj(obj, scene) + +def auto_refresh_active(scene, depsgraph=None): + if not bpy.context.scene.BSEQ.auto_refresh_active: + return + for obj in bpy.data.objects: + if obj.BSEQ.init == False: + continue + if obj.BSEQ.enabled == False: + continue + if obj.mode != "OBJECT": + continue + refresh_obj(obj, scene) + +# This becomes necessary, because when deleting objects from the viewport, they dont actually get removed from the +# sequences list, because this is not a global delete. This handler only removes sequences that are not referenced +# in any scene or collection. This handler is added to save_pre, so that unused data blocks dont get saved +def clean_unused_bseq_data(savefile): + for obj in bpy.data.objects: + if obj.BSEQ.init and len(obj.users_collection)==0 and len(obj.users_scene)==0: + + # This will throw an error if it is actually still used somewhere + bpy.data.objects.remove(obj) \ No newline at end of file diff --git a/bseq/importer.py b/bseq/importer.py new file mode 100644 index 0000000..523b6f7 --- /dev/null +++ b/bseq/importer.py @@ -0,0 +1,362 @@ +import bpy +import mathutils +import meshio +import traceback +import fileseq +import os +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 +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": + return cell.data.astype(np.uint64) + elif cell.type == "triangle6": + pass + elif cell.type == "triangle7": + pass + elif cell.type == "quad": + return cell.data.astype(np.uint64) + elif cell.type == "quad8": + pass + elif cell.type == "quad9": + pass + elif cell.type == "tetra": + data = cell.data.astype(np.uint64) + faces = data[:, [0, 2, 1]] + faces = np.append(faces, data[:, [0, 3, 2]], axis=0) + faces = np.append(faces, data[:, [0, 1, 3]], axis=0) + faces = np.append(faces, data[:, [1, 2, 3]], axis=0) + faces_copy = np.copy(faces) + faces_copy.sort(axis=1) + _, indxs, count = np.unique(faces_copy, axis=0, return_index=True, return_counts=True) + faces = faces[indxs[count == 1]] + return faces + elif cell.type == "hexahedron": + data = cell.data.astype(np.uint64) + faces = data[:, [0, 3, 2, 1]] + faces = np.append(faces, data[:, [1, 5, 4, 0]], axis=0) + faces = np.append(faces, data[:, [4, 5, 6, 7]], axis=0) + faces = np.append(faces, data[:, [3, 7, 6, 2]], axis=0) + faces = np.append(faces, data[:, [1, 2, 6, 5]], axis=0) + faces = np.append(faces, data[:, [0, 4, 7, 3]], axis=0) + faces_copy = np.copy(faces) + faces_copy.sort(axis=1) + _, indxs, count = np.unique(faces_copy, axis=0, return_index=True, return_counts=True) + faces = faces[indxs[count == 1]] + 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([]) + +def has_keyframe(obj, attr): + animdata = obj.animation_data + if animdata is not None and animdata.action is not None: + for fcurve in animdata.action.fcurves: + if fcurve.data_path == attr: + return len(fcurve.keyframe_points) > 0 + return False + +def apply_transformation(meshio_mesh, obj, depsgraph): + # evaluate the keyframe animation system + eval_location = obj.evaluated_get(depsgraph).location if has_keyframe(obj, "location") else obj.location + eval_scale = obj.evaluated_get(depsgraph).scale if has_keyframe(obj, "scale") else obj.scale + + if has_keyframe(obj, "rotation_quaternion"): + eval_rotation = obj.evaluated_get(depsgraph).rotation_quaternion + elif has_keyframe(obj, "rotation_axis_angle"): + eval_rotation = obj.evaluated_get(depsgraph).rotation_axis_angle + elif has_keyframe(obj, "rotation_euler"): + eval_rotation = obj.evaluated_get(depsgraph).rotation_euler + else: + eval_rotation = obj.rotation_euler + + eval_transform_matrix = mathutils.Matrix.LocRotScale(eval_location, eval_rotation, eval_scale) + + # evaluate the rigid body transformations (only relevant for .bin format) + rigid_body_transformation = mathutils.Matrix.Identity(4) + if meshio_mesh is not None: + if "transformation_matrix" in meshio_mesh.field_data: + rigid_body_transformation = meshio_mesh.field_data["transformation_matrix"] + + # multiply everything together (with custom transform matrix) + obj.matrix_world = rigid_body_transformation @ eval_transform_matrix + +# function to create a single custom Blender mesh attribute +def create_or_retrieve_attribute(mesh, k, v): + if k not in mesh.attributes: + if len(v) == 0: + return mesh.attributes.new(k, "FLOAT", "POINT") + if len(v.shape) == 1: + # one dimensional attribute + return mesh.attributes.new(k, "FLOAT", "POINT") + if len(v.shape) == 2: + dim = v.shape[1] + if dim > 3: + # show_message_box('higher than 3 dimensional attribue, ignored') + return None + if dim == 1: + return mesh.attributes.new(k, "FLOAT", "POINT") + if dim == 2: + return mesh.attributes.new(k, "FLOAT2", "POINT") + 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') + return None + else: + return mesh.attributes[k] + +def update_mesh(meshio_mesh, mesh): + # extract information from the meshio mesh + mesh_vertices = meshio_mesh.points + + n_poly = 0 + n_loop = 0 + n_verts = len(mesh_vertices) + if n_verts == 0: + mesh.clear_geometry() + 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: + 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 + faces_loop_start = np.roll(faces_loop_start, 1) + faces_loop_start[0] = 0 + + if len(mesh.vertices) == n_verts and len(mesh.polygons) == n_poly and len(mesh.loops) == n_loop: + pass + 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) + mesh.polygons.foreach_set("use_smooth", [shade_scheme] * len(faces_loop_total)) + + # newer function but is about 4 times slower + # mesh.clear_geometry() + # mesh.from_pydata(mesh_vertices, edge_data, face_data) + + mesh.update() + mesh.validate() + + if bpy.context.scene.BSEQ.use_imported_normals: + if "obj:vn" in meshio_mesh.point_data: + mesh.BSEQ.split_norm_att_name = "bseq_obj:vn" + elif "normals" in meshio_mesh.point_data and len(meshio_mesh.point_data["normals"]) == len(mesh.vertices): + mesh.BSEQ.split_norm_att_name = "bseq_normals" + elif "obj:vn" in meshio_mesh.field_data and "obj:vn_face_idx" in meshio_mesh.cell_data: + mesh.BSEQ.split_norm_att_name = "obj:vn" + + # copy attributes + 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" + else: + name_string = 'vector' + + attribute.data.foreach_set(name_string, v.ravel()) + + # set as split normal per vertex + if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == k: + # 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(): + if k not in mesh.attributes: + attribute = create_or_retrieve_attribute(mesh, k, []) + + # set split normal per loop per vertex + if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == k: + 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 (not a sequence, this just inports some file using meshio) +def create_meshio_obj(filepath): + meshio_mesh = None + try: + meshio_mesh = meshio.read(filepath) + except Exception as e: + show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), + "Meshio Loading Error" + str(e), + icon="ERROR") + return + # create the object + name = os.path.basename(filepath) + mesh = bpy.data.meshes.new(name) + object = bpy.data.objects.new(name, mesh) + update_mesh(meshio_mesh, object.data) + bpy.context.collection.objects.link(object) + bpy.ops.object.select_all(action="DESELECT") + bpy.context.view_layer.objects.active = object + +def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix.Identity(4)): + + current_frame = bpy.context.scene.frame_current + filepath = fileseq[current_frame % len(fileseq)] + + meshio_mesh = None + enabled = True + try: + meshio_mesh = meshio.read(filepath) + except Exception as e: + show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), + "Meshio Loading Error" + str(e), + icon="ERROR") + enabled = False + + name = fileseq.basename() + "@" + fileseq.extension() + mesh = bpy.data.meshes.new(name) + 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: + path = get_relative_path(path, root_path) + # path is only the directory in which the file is located + object.BSEQ.path = path + object.BSEQ.pattern = pattern + object.BSEQ.current_file = filepath + object.BSEQ.init = True + object.BSEQ.enabled = enabled + object.BSEQ.start_end_frame = (fileseq.start(), fileseq.end()) + object.matrix_world = transform_matrix + driver = object.driver_add("BSEQ.frame") + driver.driver.expression = 'frame' + if enabled: + update_mesh(meshio_mesh, object.data) + bpy.context.collection.objects.link(object) + bpy.ops.object.select_all(action="DESELECT") + bpy.context.view_layer.objects.active = object + +def update_obj(scene, depsgraph=None): + for obj in bpy.data.objects: + start_time = time.perf_counter() + + if obj.BSEQ.init == False: + continue + if obj.BSEQ.enabled == False: + continue + if obj.mode != "OBJECT": + continue + + if depsgraph is not None: + current_frame = obj.evaluated_get(depsgraph).BSEQ.frame + else: + show_message_box("Warning: Might not be able load the correct frame because the dependency graph is not available.", "BSEQ Warning") + current_frame = obj.BSEQ.frame + meshio_mesh = None + + # in case the blender file was created on windows system, but opened in linux system + full_path = get_absolute_path(obj, scene) + + fs = fileseq.FileSequence(full_path) + + if obj.BSEQ.use_advance and obj.BSEQ.script_name: + script = bpy.data.texts[obj.BSEQ.script_name] + try: + exec(script.as_string()) + except Exception as e: + show_message_box(traceback.format_exc(), "running script: " + obj.BSEQ.script_name + " failed: " + str(e), + "ERROR") + continue + + if 'process' in locals(): + user_process = locals()['process'] + try: + user_process(fs, current_frame, obj.data) + obj.BSEQ.current_file = "Controlled by user process" + except Exception as e: + show_message_box("Error when calling user process: " + traceback.format_exc(), icon="ERROR") + del locals()['process'] + # this continue means if process exist, all the remaining code will be ignored, whethere or not error occurs + continue + + elif 'preprocess' in locals(): + user_preprocess = locals()['preprocess'] + try: + meshio_mesh = user_preprocess(fs, current_frame) + obj.BSEQ.current_file = "Controlled by user preprocess" + except Exception as e: + show_message_box("Error when calling user preprocess: " + traceback.format_exc(), icon="ERROR") + # this continue means only if error occures, then goes to next bpy.object + continue + finally: + del locals()['preprocess'] + else: + 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") + continue + update_mesh(meshio_mesh, obj.data) + + apply_transformation(meshio_mesh, obj, depsgraph) + + end_time = time.perf_counter() + obj.BSEQ.last_benchmark = (end_time - start_time) * 1000 diff --git a/bseq/messenger.py b/bseq/messenger.py new file mode 100644 index 0000000..25cceab --- /dev/null +++ b/bseq/messenger.py @@ -0,0 +1,43 @@ +import bpy + + +def selected_callback(): + + # seems like that this is not necessary + # if not bpy.context.view_layer.objects.active: + # return + + if not bpy.context.active_object: + return + + name = bpy.context.active_object.name + idx = bpy.data.objects.find(name) + if idx >= 0: + bpy.context.scene.BSEQ.selected_obj_deselectall_flag = False + bpy.context.scene.BSEQ.selected_obj_num = idx + bpy.context.scene.BSEQ.selected_obj_deselectall_flag = True + if bpy.context.active_object.BSEQ.init: + bpy.context.scene.BSEQ.edit_obj = bpy.context.active_object + +def subscribe_to_selected(): + # import bseq + bseq = __loader__ + + # because current implementation may subscribe twice + # so clear once to avoid duplication + bpy.msgbus.clear_by_owner(bseq) + + bpy.msgbus.subscribe_rna( + key=(bpy.types.LayerObjects, 'active'), + # don't know why it needs this owner, so I set owner to this module `bseq` + owner=bseq, + # no args + args=(()), + notify=selected_callback, + ) + + +def unsubscribe_to_selected(): + # import bseq + bseq = __loader__ + bpy.msgbus.clear_by_owner(bseq) diff --git a/bseq/operators.py b/bseq/operators.py new file mode 100644 index 0000000..7168868 --- /dev/null +++ b/bseq/operators.py @@ -0,0 +1,690 @@ +import bpy +from mathutils import Matrix +import fileseq +from .messenger import * +import traceback +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" + +def relative_path_error(): + show_message_box("When using relative path, please save file before using it", icon="ERROR") + return {"CANCELLED"} + +def get_transform_matrix(importer_prop): + if importer_prop.use_custom_transform: + return Matrix.LocRotScale(importer_prop.custom_location, importer_prop.custom_rotation, importer_prop.custom_scale) + else: + return Matrix.Identity(4) + +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)) + +# Legacy import operator (this is what the "Import from folder" button does) +class BSEQ_OT_load(bpy.types.Operator): + '''Load selected sequence''' + bl_label = "Load Sequence" + bl_idname = "sequence.load" + bl_options = {"UNDO"} + + def execute(self, context): + scene = context.scene + importer_prop = scene.BSEQ + + if importer_prop.use_relative and not bpy.data.is_saved: + return relative_path_error() + + fs = importer_prop.fileseq + use_pattern = importer_prop.use_pattern + + if not use_pattern and (not fs or fs == "None"): + # fs is none + return {'CANCELLED'} + if use_pattern: + if not importer_prop.pattern: + show_message_box("Pattern is empty", icon="ERROR") + return {"CANCELLED"} + fs = importer_prop.path + '/' + importer_prop.pattern + + try: + # 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"} + + create_obj_wrapper(fs, importer_prop) + return {"FINISHED"} + + +class BSEQ_OT_edit(bpy.types.Operator): + '''Edit Sequence''' + bl_label = "Edit the path of the sequence" + bl_idname = "sequence.edit" + bl_options = {"UNDO"} + + def execute(self, context): + scene = context.scene + importer_prop = scene.BSEQ + + if importer_prop.use_relative and not bpy.data.is_saved: + # use relative but file not saved + return relative_path_error() + + fs = importer_prop.fileseq + use_pattern = importer_prop.use_pattern + + if not use_pattern and (not fs or fs == "None"): + # fs is none + return {'CANCELLED'} + if use_pattern: + if not importer_prop.pattern: + show_message_box("Pattern is empty", icon="ERROR") + return {"CANCELLED"} + fs = importer_prop.path + '/' + importer_prop.pattern + + try: + fs = fileseq.findSequenceOnDisk(fs) + except Exception as e: + show_message_box(traceback.format_exc(), "Can't find sequence: " + str(fs), "ERROR") + return {"CANCELLED"} + + sim_loader = context.scene.BSEQ + + # logic here + # it seems quite simple task, no need to create a function(for now) + obj = sim_loader.edit_obj + if not obj: + return {"CANCELLED"} + if importer_prop.use_relative: + if importer_prop.root_path != "": + object.BSEQ.pattern = bpy.path.relpath(str(fileseq), start=importer_prop.root_path) + else: + object.BSEQ.pattern = bpy.path.relpath(str(fileseq)) + + else: + obj.BSEQ.pattern = str(fs) + return {"FINISHED"} + + +class BSEQ_OT_resetpt(bpy.types.Operator): + '''This operator resets the geometry nodes of the sequence as a point cloud''' + bl_label = "Reset Geometry Nodes as Point Cloud" + bl_idname = "bseq.resetpt" + bl_options = {"UNDO"} + + def execute(self, context): + sim_loader = context.scene.BSEQ + obj = bpy.data.objects[sim_loader.selected_obj_num] + warn = False + for modifier in obj.modifiers: + if modifier.type == "NODES": + warn = True + obj.modifiers.remove(modifier) + if warn: + show_message_box("Exising geoemtry nodes of {} has been removed".format(obj.name), "Warning") + gn = obj.modifiers.new("BSEQ_GeometryNodse", "NODES") + # change starting from blender 3.2 + # https://developer.blender.org/rB08b4b657b64f + if bpy.app.version >= (3, 2, 0): + bpy.ops.node.new_geometry_node_group_assign() + gn.node_group.nodes.new('GeometryNodeMeshToPoints') + set_material = gn.node_group.nodes.new('GeometryNodeSetMaterial') + set_material.inputs[2].default_value = context.scene.BSEQ.material + + node0 = gn.node_group.nodes[0] + node1 = gn.node_group.nodes[1] + node2 = gn.node_group.nodes[2] + gn.node_group.links.new(node0.outputs[0], node2.inputs[0]) + gn.node_group.links.new(node2.outputs[0], set_material.inputs[0]) + gn.node_group.links.new(set_material.outputs[0], node1.inputs[0]) + bpy.ops.object.modifier_move_to_index(modifier=gn.name, index=0) + return {"FINISHED"} + + +class BSEQ_OT_resetmesh(bpy.types.Operator): + '''This operator resets the geometry nodes of the sequence as a point cloud''' + bl_label = "Reset Geometry Nodes as Mesh" + bl_idname = "bseq.resetmesh" + bl_options = {"UNDO"} + + def execute(self, context): + sim_loader = context.scene.BSEQ + obj = bpy.data.objects[sim_loader.selected_obj_num] + warn = False + for modifier in obj.modifiers: + if modifier.type == "NODES": + warn = True + obj.modifiers.remove(modifier) + if warn: + show_message_box("Exising geoemtry nodes of {} has been removed".format(obj.name), "Warning") + gn = obj.modifiers.new("BSEQ_GeometryNodse", "NODES") + # change starting from blender 3.2 + # https://developer.blender.org/rB08b4b657b64f + if bpy.app.version >= (3, 2, 0): + bpy.ops.node.new_geometry_node_group_assign() + bpy.ops.object.modifier_move_to_index(modifier=gn.name, index=0) + return {"FINISHED"} + + +class BSEQ_OT_resetins(bpy.types.Operator): + '''This operator resets the geometry nodes of the sequence as a point cloud''' + bl_label = "Reset Geometry Nodes as Instances" + bl_idname = "bseq.resetins" + bl_options = {"UNDO"} + + def execute(self, context): + sim_loader = context.scene.BSEQ + obj = bpy.data.objects[sim_loader.selected_obj_num] + warn = False + for modifier in obj.modifiers: + if modifier.type == "NODES": + warn = True + obj.modifiers.remove(modifier) + if warn: + show_message_box("Exising geoemtry nodes of {} has been removed".format(obj.name), "Warning") + gn = obj.modifiers.new("BSEQ_GeometryNodse", "NODES") + # change starting from blender 3.2 + # https://developer.blender.org/rB08b4b657b64f + if bpy.app.version >= (3, 2, 0): + bpy.ops.node.new_geometry_node_group_assign() + nodes = gn.node_group.nodes + links = gn.node_group.links + input_node = nodes[0] + output_node = nodes[1] + + instance_on_points = nodes.new('GeometryNodeInstanceOnPoints') + cube = nodes.new('GeometryNodeMeshCube') + realize_instance = nodes.new('GeometryNodeRealizeInstances') + set_material = nodes.new('GeometryNodeSetMaterial') + + instance_on_points.inputs['Scale'].default_value = [ + 0.05, + 0.05, + 0.05, + ] + set_material.inputs[2].default_value = context.scene.BSEQ.material + + links.new(input_node.outputs[0], instance_on_points.inputs['Points']) + links.new(cube.outputs[0], instance_on_points.inputs['Instance']) + links.new(instance_on_points.outputs[0], realize_instance.inputs[0]) + links.new(realize_instance.outputs[0], set_material.inputs[0]) + links.new(set_material.outputs[0], output_node.inputs[0]) + + bpy.ops.object.modifier_move_to_index(modifier=gn.name, index=0) + return {"FINISHED"} + + +class BSEQ_OT_set_as_split_norm(bpy.types.Operator): + '''Set vertex attribute as vertex split normals''' + bl_label = "Set as split normal per vertex" + bl_idname = "bseq.setsplitnorm" + bl_options = {"UNDO"} + + def execute(self, context): + sim_loader = context.scene.BSEQ + obj = bpy.data.objects[sim_loader.selected_obj_num] + mesh = obj.data + attr_index = sim_loader.selected_attribute_num + if attr_index >= len(mesh.attributes): + show_message_box("Please select the attribute") + return {"CANCELLED"} + mesh.BSEQ.split_norm_att_name = mesh.attributes[attr_index].name + + return {"FINISHED"} + + +class BSEQ_OT_remove_split_norm(bpy.types.Operator): + '''Remove vertex attribute as vertex split normals''' + bl_label = "Remove split normal per vertex" + bl_idname = "bseq.removesplitnorm" + bl_options = {"UNDO"} + + def execute(self, context): + sim_loader = context.scene.BSEQ + obj = bpy.data.objects[sim_loader.selected_obj_num] + mesh = obj.data + if mesh.BSEQ.split_norm_att_name: + mesh.BSEQ.split_norm_att_name = "" + + return {"FINISHED"} + + +class BSEQ_OT_disable_selected(bpy.types.Operator): + '''Deactivate selected sequences''' + bl_label = "Deactivate sequence" + bl_idname = "bseq.disableselected" + bl_options = {"UNDO"} + + def execute(self, context): + for obj in bpy.context.selected_objects: + if obj.BSEQ.init and obj.BSEQ.enabled: + obj.BSEQ.enabled = False + return {"FINISHED"} + + +class BSEQ_OT_enable_selected(bpy.types.Operator): + '''Activate selected sequences''' + bl_label = "Activate sequence" + bl_idname = "bseq.enableselected" + bl_options = {"UNDO"} + + def execute(self, context): + for obj in bpy.context.selected_objects: + if obj.BSEQ.init and not obj.BSEQ.enabled: + obj.BSEQ.enabled = True + return {"FINISHED"} + + +class BSEQ_OT_refresh_seq(bpy.types.Operator): + '''Refresh selected sequences''' + bl_label = "Refresh sequence" + bl_idname = "bseq.refresh" + + def execute(self, context): + scene = context.scene + obj = bpy.data.objects[scene.BSEQ.selected_obj_num] + refresh_obj(obj, scene) + + return {"FINISHED"} + +class BSEQ_OT_disable_all(bpy.types.Operator): + '''Deactivate all sequences''' + bl_label = "Deactivate all sequences" + bl_idname = "bseq.disableall" + bl_options = {"UNDO"} + + def execute(self, context): + for obj in bpy.context.scene.collection.all_objects: + if obj.BSEQ.init and obj.BSEQ.enabled: + obj.BSEQ.enabled = False + return {"FINISHED"} + +class BSEQ_OT_enable_all(bpy.types.Operator): + '''Activate all sequences''' + bl_label = "Activate all sequences" + bl_idname = "bseq.enableall" + bl_options = {"UNDO"} + + def execute(self, context): + for obj in bpy.context.scene.collection.all_objects: + if obj.BSEQ.init and not obj.BSEQ.enabled: + obj.BSEQ.enabled = True + return {"FINISHED"} + +class BSEQ_OT_refresh_sequences(bpy.types.Operator): + '''Refresh all sequences''' + bl_label = "Reloads everything in selected folder" + bl_idname = "bseq.refreshall" + bl_options = {"UNDO"} + + def execute(self, context): + scene = context.scene + # call the update function of path by setting it to its own value + scene.BSEQ.path = scene.BSEQ.path + + return {"FINISHED"} + +class BSEQ_OT_set_start_end_frames(bpy.types.Operator): + '''This operator changes the timeline start and end frames to the length of a specific sequence''' + bl_label = "Set timeline" + bl_idname = "bseq.set_start_end_frames" + bl_options = {"UNDO"} + + def execute(self, context): + scene = context.scene + obj = bpy.data.objects[scene.BSEQ.selected_obj_num] + (start, end) = obj.BSEQ.start_end_frame + scene.frame_start = 0 + scene.frame_end = end - start + + return {"FINISHED"} + +from pathlib import Path +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): + """Import one or multiple sequences""" + bl_idname = "wm.seq_import_batch" + bl_label = "Import Sequences" + bl_options = {'PRESET', 'UNDO'} + + def update_filter_glob(self, context): + bpy.ops.wm.seq_import_batch('INVOKE_DEFAULT') + + filter_string: bpy.props.StringProperty( + default="*.obj", + options={'HIDDEN'}, + update=update_filter_glob, + ) + + filename_ext='' + filter_glob: bpy.props.StringProperty( + default='*.obj', + options={'HIDDEN', 'LIBRARY_EDITABLE'}, + ) + + files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + + def invoke(self, context, event): + scene = context.scene + if scene.BSEQ.filter_string: + self.filter_glob = scene.BSEQ.filter_string + else: + self.filter_glob = "*" + + context.window_manager.fileselect_add(self) + + return {'RUNNING_MODAL'} + + def execute(self, context): + scene = context.scene + importer_prop = scene.BSEQ + + if importer_prop.use_relative and not bpy.data.is_saved: + return relative_path_error() + + self.filter_glob = '*' + + folder = Path(self.filepath) + used_seqs = set() + + for selection in self.files: + # Check if there exists a matching file sequence for every selection + fp = str(Path(folder.parent, selection.name)) + seqs = fileseq.findSequencesOnDisk(str(folder.parent)) + matching_seq = [s for s in seqs if fp in list(s) and str(s) not in used_seqs] + + if matching_seq: + matching_seq = matching_seq[0] + used_seqs.add(str(matching_seq)) + + create_obj_wrapper(matching_seq, importer_prop) + return {'FINISHED'} + + def draw(self, context): + pass + +class BSEQ_PT_batch_sequences_settings(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "Settings Panel" + bl_options = {'HIDE_HEADER'} + # bl_parent_id = "FILE_PT_operator" # Optional + + @classmethod + def poll(cls, context): + sfile = context.space_data + operator = sfile.active_operator + return operator.bl_idname == "WM_OT_seq_import_batch" + + def draw(self, context): + layout = self.layout + importer_prop = context.scene.BSEQ + + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + # # sfile = context.space_data + # # operator = sfile.active_operator + + # layout.prop(importer_prop, 'filter_string') + + # layout.alignment = 'LEFT' + # layout.prop(importer_prop, "relative", text="Relative Path") + # if importer_prop.use_relative: + # layout.prop(importer_prop, "root_path", text="Root Directory") + +class BSEQ_addon_preferences(bpy.types.AddonPreferences): + bl_idname = __package__ + + zips_folder: bpy.props.StringProperty( + name="Zips Folder", + subtype='DIR_PATH', + ) + + def draw(self, context): + # layout = self.layout + # layout.label(text="Please set a folder to store the extracted zip files") + # layout.prop(self, "zips_folder", text="Zips Folder") + pass + +zip_folder_name = '/tmp_zips' + +class BSEQ_OT_import_zip(bpy.types.Operator, ImportHelper): + """Import a zip file""" + bl_idname = "bseq.import_zip" + bl_label = "Import Zip" + bl_options = {'PRESET', 'UNDO'} + + filename_ext = ".zip" + filter_glob: bpy.props.StringProperty( + default="*.zip", + options={'HIDDEN', 'LIBRARY_EDITABLE'}, + ) + + files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + + def execute(self, context): + importer_prop = context.scene.BSEQ + + import zipfile + zip_file = zipfile.ZipFile(self.filepath) + + addon_prefs = context.preferences.addons[addon_name].preferences + # Check if a string is empty: + if not addon_prefs.zips_folder: + show_message_box("Please set a folder to store the extracted zip files", icon="ERROR") + return {"CANCELLED"} + zips_folder = addon_prefs.zips_folder + zip_folder_name + + valid_files = [info.filename for info in zip_file.infolist() if not info.filename.startswith('__MACOSX/')] + zip_file.extractall(zips_folder, members=valid_files) + zip_file.close() + + folder = str(zips_folder) + '/' + str(Path(self.filepath).name)[:-4] + print(folder) + + seqs = fileseq.findSequencesOnDisk(str(folder)) + if not seqs: + show_message_box("No sequences found in the zip file", icon="ERROR") + return {"CANCELLED"} + + for s in seqs: + # Import it with absolute paths + create_obj(s, False, folder, transform_matrix=get_transform_matrix(importer_prop)) + + # created_folder = context.scene.BSEQ.imported_zips.add() + # created_folder.path = folder + + return {'FINISHED'} + +class BSEQ_OT_delete_zips(bpy.types.Operator): + """Delete a zip file""" + bl_idname = "bseq.delete_zips" + bl_label = "Delete Zip" + bl_options = {'PRESET', 'UNDO'} + + def execute(self, context): + # folders = context.scene.BSEQ.imported_zips + # for folder in folders: + + addon_prefs = context.preferences.addons[addon_name].preferences + zips_folder = addon_prefs.zips_folder + zip_folder_name + + import shutil + shutil.rmtree(zips_folder) + + return {'FINISHED'} + +class BSEQ_OT_load_all(bpy.types.Operator): + """Load all sequences from selected folder and its subfolders""" + bl_idname = "bseq.load_all" + bl_label = "Load All" + 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() + + p = importer_prop.path + seqs = fileseq.findSequencesOnDisk(bpy.path.abspath(p)) + + for s in seqs: + print(s) + + 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): + """Batch Import Meshio Objects""" + bl_idname = "wm.meshio_import_batch" + bl_label = "Import Multiple Meshio Objects" + bl_options = {'PRESET', 'UNDO'} + + files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + + def execute(self, context): + folder = Path(self.filepath) + + for selection in self.files: + fp = Path(folder.parent, selection.name) + create_meshio_obj(str(fp)) + return {'FINISHED'} + +def menu_func_import(self, context): + self.layout.operator( + BSEQ_OT_meshio_object.bl_idname, + text="MeshIO Object") + +# Default Keymap Configuration +addon_keymaps = [] + +def add_keymap(): + wm = bpy.context.window_manager + + # Add new keymap section for BSEQ + + kc = wm.keyconfigs.addon + if kc: + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("sequence.load", type='F', value='PRESS', shift=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.disableselected", type='D', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.enableselected", type='E', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.refresh", type='R', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.disableall", type='D', value='PRESS', shift=True, alt=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.enableall", type='E', value='PRESS', shift=True, alt=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.refreshall", type='R', value='PRESS', shift=True, alt=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.set_start_end_frames", type='F', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("wm.seq_import_batch", type='I', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("wm.meshio_import_batch", type='M', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + +def delete_keymap(): + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() diff --git a/bseq/panels.py b/bseq/panels.py new file mode 100644 index 0000000..c70cdd2 --- /dev/null +++ b/bseq/panels.py @@ -0,0 +1,351 @@ +import bpy +import os + +class BSEQ_UL_Obj_List(bpy.types.UIList): + ''' + This controls the list of imported sequences. + ''' + + def filter_items(self, context, data, property): + objs = getattr(data, property) + flt_flags = [] + # not sure if I understand correctly about this + # see reference from https://docs.blender.org/api/current/bpy.types.UIList.html#advanced-uilist-example-filtering-and-reordering + for o in objs: + if o.BSEQ.init and len(o.users_collection)>0 and len(o.users_scene)>0: + flt_flags.append(self.bitflag_filter_item) + else: + flt_flags.append(0) + flt_neworder = [] + return flt_flags, flt_neworder + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + split = layout.split(factor=0.4) + col1 = split.column() + col2 = split.column() + split2 = col2.split(factor=0.25) + col2 = split2.column() + col3 = split2.column() + split3 = col3.split(factor=0.5) + col3 = split3.column() + col4 = split3.column() + col4.alignment = 'EXPAND' + start_frame = item.BSEQ.start_end_frame[0] + end_frame = item.BSEQ.start_end_frame[1] + + col1.prop(item, "name", text='', emboss=False) + if item.BSEQ.enabled: + col2.prop(item.BSEQ, "enabled", text="", icon="PLAY") + col3.prop(item.BSEQ, "frame", text="") + col4.label(text=str(start_frame) + '-' + str(end_frame)) + else: + col2.prop(item.BSEQ, "enabled", text ="", icon="PAUSE") + col3.label(text="", icon="BLANK1") + col4.label(text=str(start_frame) + '-' + str(end_frame)) + else: + # actually, I guess this line of code won't be executed? + layout.label(text="", translate=False, icon_value=icon) + +class BSEQ_UL_Att_List(bpy.types.UIList): + ''' + This controls the list of attributes available for this sequence + ''' + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + if item: + layout.enabled = False + layout.prop(item, "name", text='', emboss=False) + obj = bpy.data.objects[context.scene.BSEQ.selected_obj_num] + mesh = obj.data + if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == item.name: + layout.label(text="Use as split norm.") + + else: + # actually, I guess this line of code won't be executed? + layout.label(text="", translate=False, icon_value=icon) + +class BSEQ_Panel: + bl_space_type = 'VIEW_3D' + bl_region_type = "UI" + bl_category = "Sequence Loader" + bl_context = "objectmode" + +class BSEQ_Globals_Panel(BSEQ_Panel, bpy.types.Panel): + bl_label = "Global Settings" + bl_idname = "BSEQ_PT_global" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + sim_loader = context.scene.BSEQ + split = layout.split() + col1 = split.column() + col1.alignment = 'RIGHT' + col2 = split.column() + + col1.label(text="Root Directory") + col2.prop(sim_loader, "root_path", text="") + col1.label(text="Print Sequence Information") + col2.prop(sim_loader, "print", text="") + col1.label(text="Auto Refresh Active") + col2.prop(sim_loader, "auto_refresh_active", text="") + col1.label(text="Auto Refresh All") + col2.prop(sim_loader, "auto_refresh_all", text="") + +class BSEQ_Advanced_Panel(BSEQ_Panel, bpy.types.Panel): + bl_label = "Advanced Settings" + bl_idname = "BSEQ_PT_advanced" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + sim_loader = context.scene.BSEQ + + split = layout.split() + col1 = split.column() + col2 = split.column() + + if sim_loader.selected_obj_num >= len(bpy.data.objects): + return + obj = bpy.data.objects[sim_loader.selected_obj_num] + if not obj.BSEQ.init: + return + + col1.label(text='Script') + col2.prop_search(obj.BSEQ, 'script_name', bpy.data, 'texts', text="") + + # geometry nodes settings + layout.label(text="Geometry Nodes (select sequence first)") + + box = layout.box() + box.label(text="Point Cloud and Instances Material") + split = box.split() + col1 = split.column() + col1.alignment = 'RIGHT' + col2 = split.column() + col1.label(text="Material") + col2.prop_search(sim_loader, 'material', bpy.data, 'materials', text="") + box.label(text='Reset Geometry Nodes to') + + split = box.split() + col1 = split.column() + col2 = split.column() + col3 = split.column() + col1.operator('bseq.resetpt', text="Point Cloud") + col2.operator('bseq.resetmesh', text="Mesh") + col3.operator('bseq.resetins', text="Instances") + + +class BSEQ_List_Panel(BSEQ_Panel, bpy.types.Panel): + ''' + This is the panel of imported sequences, bottom part of images/9.png + ''' + bl_label = "Sequences" + bl_idname = "BSEQ_PT_list" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + sim_loader = context.scene.BSEQ + row = layout.row() + row.template_list("BSEQ_UL_Obj_List", "", bpy.data, "objects", sim_loader, "selected_obj_num", rows=2) + row = layout.row() + row.operator("bseq.enableselected", text="Activate") + row.operator("bseq.disableselected", text="Deactivate") + row.operator("bseq.refresh", text="Refresh") + row = layout.row() + row.operator("bseq.enableall", text="Activate All") + row.operator("bseq.disableall", text="Deactivate All") + row.operator("bseq.set_start_end_frames", text="Set timeline") + +class BSEQ_Settings(BSEQ_Panel, bpy.types.Panel): + ''' + This is the panel of settings of selected sequence + ''' + bl_label = "Sequence Properties" + bl_idname = "BSEQ_PT_settings" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + sim_loader = context.scene.BSEQ + importer_prop = context.scene.BSEQ + + if sim_loader.selected_obj_num >= len(bpy.data.objects): + return + obj = bpy.data.objects[sim_loader.selected_obj_num] + if not obj.BSEQ.init: + return + + split = layout.split() + col1 = split.column() + 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') + col2.prop(obj.BSEQ, 'pattern', text="") + # Read-only + col1.label(text='Current File') + # make it read-only + row1 = col2.row() + row1.enabled = False + row1.prop(obj.BSEQ, 'current_file', text="") + col1.label(text='Last loading time (ms)') + row2 = col2.row() + row2.enabled = False + row2.prop(obj.BSEQ, 'last_benchmark', text="", ) + + # attributes settings + layout.label(text="Attributes") + box = layout.box() + row = box.row() + row.template_list("BSEQ_UL_Att_List", "", obj.data, "attributes", sim_loader, "selected_attribute_num", rows=2) + box.operator("bseq.setsplitnorm", text="Set selected as normal") + box.operator("bseq.removesplitnorm", text="Clear normal") + +class BSEQ_PT_Import(BSEQ_Panel, bpy.types.Panel): + ''' + This is the panel of main addon interface. see images/1.jpg + ''' + bl_label = "Import" + bl_idname = "BSEQ_PT_panel" + + def draw(self, context): + layout = self.layout + scene = context.scene + importer_prop = scene.BSEQ + + row = layout.row() + + row.scale_y = 1.5 + row.operator("wm.seq_import_batch") + + split = layout.split() + col1 = split.column() + col2 = split.column() + + split = layout.split(factor=0.5) + col1 = split.column() + col1.alignment = 'RIGHT' + col2 = split.column(align=False) + + # col2.prop(importer_prop, "filter_string", text="Filter String") + + col1.label(text="Relative Paths") + col2.prop(importer_prop, "use_relative", text="") + + 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") + col2.prop(importer_prop, "use_custom_transform", text="") + + if importer_prop.use_custom_transform: + split = layout.split(factor=0.33) + box_col1 = split.column() + box_col2 = split.column() + box_col3 = split.column() + + box_col1.label(text="Location:") + box_col1.prop(importer_prop, "custom_location", text="") + + box_col2.label(text="Rotation:") + box_col2.prop(importer_prop, "custom_rotation", text="") + + box_col3.label(text="Scale:") + box_col3.prop(importer_prop, "custom_scale", text="") + +class BSEQ_PT_Import_Child1(BSEQ_Panel, bpy.types.Panel): + bl_parent_id = "BSEQ_PT_panel" + bl_label = "Import from folder" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + importer_prop = scene.BSEQ + + split = layout.split() + col1 = split.column() + col1.alignment = 'RIGHT' + col2 = split.column(align=False) + + col1.label(text="Directory") + col2.prop(importer_prop, "path", text="") + + col1.label(text="Custom Pattern") + col2.prop(importer_prop, "use_pattern", text="") + col1.label(text="Sequence Pattern") + if importer_prop.use_pattern: + col2.prop(importer_prop, "pattern", text="") + else: + split2 = col2.split(factor=0.75) + col3 = split2.column() + col4 = split2.column() + col3.prop(importer_prop, "fileseq", text="") + col4.operator("bseq.refreshall", text='', icon="FILE_REFRESH") + + split = layout.split(factor=0.5) + col1 = split.column() + col2 = split.column() + col1.operator("sequence.load") + row = col2.row() + row.operator("bseq.load_all") + row.operator("bseq.load_all_recursive") + + # split = layout.split(factor=0.5) + # col1 = split.column() + # col2 = split.column() + + # col1.operator("bseq.import_zip", text="Import from zip") + # col2.operator("bseq.delete_zips", text="Delete created folders") + + +class BSEQ_PT_Import_Child2(BSEQ_Panel, bpy.types.Panel): + bl_parent_id = "BSEQ_PT_panel" + bl_label = "Test" + bl_options = {'HIDE_HEADER'} + + def draw(self, context): + layout = self.layout + scene = context.scene + importer_prop = scene.BSEQ + + split = layout.split() + col1 = split.column() + col2 = split.column() + +class BSEQ_Templates(bpy.types.Menu): + ''' + Here is the template panel, shown in the text editor -> templates + ''' + bl_label = "Sequence Loader" + bl_idname = "BSEQ_MT_template" + + def draw(self, context): + current_folder = os.path.dirname(os.path.abspath(__file__)) + self.path_menu( + # it goes to current folder -> parent folder -> template folder + [current_folder + '/../template'], + "text.open", + props_default={"internal": True}, + ) + + +def draw_template(self, context): + ''' + Here it function call to integrate template panel into blender template interface + ''' + layout = self.layout + layout.menu(BSEQ_Templates.bl_idname) \ No newline at end of file diff --git a/bseq/properties.py b/bseq/properties.py new file mode 100644 index 0000000..dd93647 --- /dev/null +++ b/bseq/properties.py @@ -0,0 +1,138 @@ +import bpy +from .callback import * +from mathutils import Matrix + +class BSEQ_scene_property(bpy.types.PropertyGroup): + path: bpy.props.StringProperty(name="Directory", + subtype="DIR_PATH", + description="You need to go to the folder with the sequence, then click \"Accept\"", + update=update_path, + options={'PATH_SUPPORTS_BLEND_RELATIVE' if bpy.app.version >= (4, 5, 0) else ''} + ) + + use_relative: bpy.props.BoolProperty(name='Relative Paths', + description="Toggle relative paths on/off", + default=False, + ) + + 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 root folder for all relative paths. If empty, root is folder of the Blender file", + update=update_path, + default="", + options={'PATH_SUPPORTS_BLEND_RELATIVE' if bpy.app.version >= (4, 5, 0) else ''} + ) + + fileseq: bpy.props.EnumProperty( + name="File Sequences", + description="Select a file sequence", + items=item_fileseq, + ) + + 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. 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='Sequences List', + default=0, + update=update_selected_obj_num, + ) + + 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, + ) + + edit_obj: bpy.props.PointerProperty( + type=bpy.types.Object, + poll=poll_edit_obj, + ) + + print: bpy.props.BoolProperty(name='Print Sequence Information', + description="Print useful information during rendering to file in same folder as render output", + default=True, + ) + + auto_refresh_active: bpy.props.BoolProperty(name='Auto Refresh Active Sequences', + description="Auto refresh all active sequences every frame", + default=False, + ) + + auto_refresh_all: bpy.props.BoolProperty(name='Auto Refresh All Sequences', + description="Auto refresh all sequences every frame", + default=False, + ) + + use_custom_transform: bpy.props.BoolProperty(name='Custom Transform', + description="Use a custom transformation matrix when importing", + default=False, + ) + + custom_location: bpy.props.FloatVectorProperty(name='Custom Location', + description='Set custom location vector', + size=3, + subtype="TRANSLATION", + ) + + custom_rotation: bpy.props.FloatVectorProperty(name='Custom Rotation', + description='Set custom Euler angles', + size=3, + subtype="EULER", + default=[0,0,0], + ) + + custom_scale: bpy.props.FloatVectorProperty(name='Custom Scale', + description='Set custom scaling vector', + size=3, + subtype="COORDINATES", + default=[1,1,1], + ) + + use_blender_obj_import: bpy.props.BoolProperty(name='Blender .obj import', + description="Use Blender's built-in .obj import function (or meshio's .obj import function)", + default=True, + ) + + filter_string: bpy.props.StringProperty(name='Filter String', + description='Filter string for file sequences', + default='', + ) + +class BSEQ_obj_property(bpy.types.PropertyGroup): + init: bpy.props.BoolProperty(default=False) + enabled: bpy.props.BoolProperty(default=True, + 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(name="Script name") + path: bpy.props.StringProperty(name="Path of sequence", subtype="DIR_PATH", options={'PATH_SUPPORTS_BLEND_RELATIVE' if bpy.app.version >= (4, 5, 0) else ''}) + 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?) +class BSEQ_mesh_property(bpy.types.PropertyGroup): + split_norm_att_name: bpy.props.StringProperty(default="") diff --git a/bseq/utils.py b/bseq/utils.py new file mode 100644 index 0000000..161542c --- /dev/null +++ b/bseq/utils.py @@ -0,0 +1,77 @@ +import bpy +import fileseq +import os +import meshio +import traceback + +def show_message_box(message="", title="Message Box", icon="INFO"): + ''' + It shows a small window to display the error message and also print it the console + ''' + + def draw(self, context): + lines = message.splitlines() + for line in lines: + self.layout.label(text=line) + + print("Information: ", title) + print(message) + print('End of bseq message box') + print() + 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 + bpy.ops.screen.animation_cancel() + +def get_relative_path(path, root_path): + if root_path != "": + rel_path = bpy.path.relpath(path, start=bpy.path.abspath(root_path)) + else: + 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 = os.path.abspath(bpy.path.abspath(path, start=bpy.path.abspath(root_path))) + else: + path = os.path.abspath(bpy.path.abspath(path)) + return path + +def get_absolute_path(obj, scene): + full_path = os.path.join(bpy.path.native_pathsep(obj.BSEQ.path), obj.BSEQ.pattern) + 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("//") + fs = get_absolute_path(obj, scene) + fs = fileseq.findSequenceOnDisk(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: + 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/build_addon.py b/build_addon.py index 93fe126..db5d1b6 100644 --- a/build_addon.py +++ b/build_addon.py @@ -2,14 +2,14 @@ import os from datetime import date - -addondirectory = 'simloader' +addondirectory = 'bseq' templatedirectory = 'template' additionaldirectory = 'additional_file_formats' meshiodirectory = 'extern/meshio/src/meshio' fileseqdirectory = 'extern/fileseq/src/fileseq' futuredirectory = 'extern/python-future/src/future' richdirectory = 'extern/rich/rich' +foldername = 'blendersequenceloader/' dirs = { addondirectory: addondirectory, @@ -32,8 +32,8 @@ filepath = os.path.join(subdir, file) relative_path = os.path.relpath(filepath, k) endpath = os.path.join(v, relative_path) - endpath = os.path.join('simloaderaddon/', endpath) + endpath = os.path.join(foldername, endpath) addonzip.write(filepath, endpath) # write init.py - addonzip.write('__init__.py', 'simloaderaddon/__init__.py') + addonzip.write('__init__.py', foldername + '__init__.py') diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/about.md b/docs/about.md new file mode 100644 index 0000000..a091b68 --- /dev/null +++ b/docs/about.md @@ -0,0 +1,50 @@ +# Description + +This is an addon for Blender 3.1+ (it might work with 2.8+ but has not been tested) that enables loading of file sequences. The addon comes bundled together with [meshio](https://github.com/nschloe/meshio) which enables the loading of geometric data from a multitude of [file formats](./format.md). + +All data is loaded *just-in-time* when the Blender frame changes, in order to avoid excessive memory consumption. By default, the addon is able to load vertices, lines, triangles and quads. It is also able to automatically extract triangle and quad surface meshes from tetrahedral and hexahedral volume meshes. Scalar and vector attributes on vertices are also imported for visualization purposes. + +## Basic usage + +This video shows the basic usage of this addon, i.e. how to load and render a simple sequence of particle data + +![usage](../images/usage.gif) + +## Affected Blender Settings + +This addon will change the value of `Preferences`->`Save & Load` ->`Default To` ->`Relative Paths` to `false`. Default value is `true`. For information can be found [here](https://docs.blender.org/manual/en/latest/editors/preferences/save_load.html#blend-files). + +This addon will also modify the `sys.path` variable of Blender python environment, by inserting the path of the addon itself. This makes it possible to use the bundled libraries. + +## Dependencies + +| name | link | license | description | +| ------------- | ------------------------------------------------------- | ------- | --------------------------- | +| meshio | [link](https://github.com/nschloe/meshio) | MIT | Loading mesh data | +| fileseq | [link](https://github.com/justinfx/fileseq) | MIT | Detection of file sequences | +| rich | [link](https://github.com/Textualize/rich) | MIT | dependency of meshio | +| python-future | [link](https://github.com/PythonCharmers/python-future) | MIT | dependency of fileseq | + +## License + +MIT License + +Copyright (c) 2022 Interactive Computer Graphics + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..269e963 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,52 @@ +# Build and install the addon + +## Build from source + +1. Clone the project to download both project and dependencies + +```shell +git clone https://github.com/InteractiveComputerGraphics/blender-sequence-loader.git --recurse-submodules +``` + +2. Build the installable .zip file by simply running the following command. This should produce a file called `blender_sequence_loader_{date}.zip`, where `{date}` is replaced with todays date. No other dependency other than standard python3 libraries are needed to build the addon. + +```python +python3 build_addon.py +``` + + +## Download from release page + +Or you can simply download the latest `.zip` file from the [releases](https://github.com/InteractiveComputerGraphics/blender-sequence-loader/releases) page. + +## Install the zip file + +You can check the official Blender documentation [here](https://docs.blender.org/manual/en/latest/editors/preferences/addons.html#installing-add-ons) for installing and enabling addons. + +## For developers + +If you want to develop this addon, using soft link (on Linux/macOS) / [Symlinks](https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/) (on Windows) can be very helpful. + +### *-nix Users + +Once you have cloned the project, go to the root directory of this addon, you can create symlink as follows +```bash +ln -s extern/meshio/src/meshio meshio +ln -s extern/rich/rich/ rich +ln -s extern/fileseq/src/fileseq fileseq +ln -s extern/python-future/src/future/ future +``` + +Then create a soft link to link from the [blender addon directory](https://docs.blender.org/manual/en/latest/advanced/blender_directory_layout.html)[^1] to the directory where you download and unzip the files. For example this could look like this on MacOS, + +```bash +ln -s ~/Downloads/blender-sequence-loader ~/Library/Application Support/Blender/3.1/scripts/addons/blender-sequence-loader-dev +``` + +[^1]: By default, `{USER}/scripts/addons`, `{USER}`: Location of configuration files (typically in the user’s home directory). + +### Windows Users + +You can use [mklink](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/mklink) to do the same things as *-nix users. [^2] + +[^2]: You will need either administrator permission, or [developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development). diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..cedad4d --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,36 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'blender-sequence-loader' +copyright = '2025, InteractiveComputerGraphics' +author = 'InteractiveComputerGraphics' +release = '0.3.6' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ['myst_parser','sphinx_rtd_theme'] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ['_static'] +source_suffix = { + '.rst': 'restructuredtext', + '.txt': 'markdown', + '.md': 'markdown', +} + + +myst_heading_anchors = 2 diff --git a/docs/format.md b/docs/format.md new file mode 100644 index 0000000..d822b68 --- /dev/null +++ b/docs/format.md @@ -0,0 +1,54 @@ +# File Formats + +The current file formats supported by [meshio](https://github.com/nschloe/meshio) [^1] are + +> [Abaqus](http://abaqus.software.polimi.it/v6.14/index.html) (`.inp`), +> ANSYS msh (`.msh`), +> [AVS-UCD](https://lanl.github.io/LaGriT/pages/docs/read_avs.html) (`.avs`), +> [CGNS](https://cgns.github.io/) (`.cgns`), +> [DOLFIN XML](https://manpages.ubuntu.com/manpages/jammy/en/man1/dolfin-convert.1.html) (`.xml`), +> [Exodus](https://nschloe.github.io/meshio/exodus.pdf) (`.e`, `.exo`), +> [FLAC3D](https://www.itascacg.com/software/flac3d) (`.f3grid`), +> [H5M](https://www.mcs.anl.gov/~fathom/moab-docs/h5mmain.html) (`.h5m`), +> [Kratos/MDPA](https://github.com/KratosMultiphysics/Kratos/wiki/Input-data) (`.mdpa`), +> [Medit](https://people.sc.fsu.edu/~jburkardt/data/medit/medit.html) (`.mesh`, `.meshb`), +> [MED/Salome](https://docs.salome-platform.org/latest/dev/MEDCoupling/developer/med-file.html) (`.med`), +> [Nastran](https://help.autodesk.com/view/NSTRN/2019/ENU/?guid=GUID-42B54ACB-FBE3-47CA-B8FE-475E7AD91A00) (bulk data, `.bdf`, `.fem`, `.nas`), +> [Netgen](https://github.com/ngsolve/netgen) (`.vol`, `.vol.gz`), +> [Neuroglancer precomputed format](https://github.com/google/neuroglancer/tree/master/src/neuroglancer/datasource/precomputed#mesh-representation-of-segmented-object-surfaces), +> [Gmsh](https://gmsh.info/doc/texinfo/gmsh.html#File-formats) (format versions 2.2, 4.0, and 4.1, `.msh`), +> [OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) (`.obj`), +> [OFF](https://segeval.cs.princeton.edu/public/off_format.html) (`.off`), +> [PERMAS](https://www.intes.de) (`.post`, `.post.gz`, `.dato`, `.dato.gz`), +> [PLY]() (`.ply`), +> [STL]() (`.stl`), +> [Tecplot .dat](http://paulbourke.net/dataformats/tp/), +> [TetGen .node/.ele](https://wias-berlin.de/software/tetgen/fformats.html), +> [SVG](https://www.w3.org/TR/SVG/) (2D output only) (`.svg`), +> [SU2](https://su2code.github.io/docs_v7/Mesh-File/) (`.su2`), +> [UGRID](https://www.simcenter.msstate.edu/software/documentation/ug_io/3d_grid_file_type_ugrid.html) (`.ugrid`), +> [VTK](https://vtk.org/wp-content/uploads/2015/04/file-formats.pdf) (`.vtk`), +> [VTU](https://vtk.org/Wiki/VTK_XML_Formats) (`.vtu`), +> [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) ([TIN](https://en.wikipedia.org/wiki/Triangulated_irregular_network)) (`.wkt`), +> [XDMF](https://xdmf.org/index.php/XDMF_Model_and_Format) (`.xdmf`, `.xmf`). + +The additionally supported file formats are + +> [bgeo](https://github.com/wdas/partio)(`.bgeo`) [^2] +> [mzd](https://github.com/InteractiveComputerGraphics/MayaMeshTools/tree/main/extern/mzd)(`.mzd`) + +[^1]: Not all of the formats have been tested for this addon and some issues may still occur. + +[^2]: The addon only supports particle-only `.bgeo` files + +## Add support for customized file formats + +You can add support for your own customized file formats. For example, if you want to support `.example` file formats. + +To do that, +1. Create a `example.py` file in the folder additional_file_formats +2. Implement a function `def readexample_to_meshio(filepath):`, which reads the file from `filepath`, then construct a [meshio.Mesh](https://github.com/nschloe/meshio/wiki/meshio-Mesh()-data-structure) object. +3. Add `meshio.register_format("example", [".example"], readexample_to_meshio, {".example": None})` in the global space +4. Add `from . import example` in `additional_file_formats/__init__.py` + +You can check [additional_file_formats/bgeo.py](https://github.com/InteractiveComputerGraphics/blender-sequence-loader/blob/main/additional_file_formats/bgeo.py) as an example. diff --git a/docs/frame.rst b/docs/frame.rst new file mode 100644 index 0000000..05753f4 --- /dev/null +++ b/docs/frame.rst @@ -0,0 +1,20 @@ +Frame control +============= + +You can use Blenders `driver system `_ to control the frame of the sequence. + +Default settings +***************** + +Each sequence has its own property ``Current Frame`` to control its frame. By default, the value equals to the `blender current frame `_. + +.. image:: ../images/current_frame.png + :align: center + +Change the value +***************** + +Right click on the ``Current Frame`` property, then click ``Edit Driver``. You can check `here `_ for more details about how to edit the driver. + +.. image:: ../images/edit_driver.png + :align: center diff --git a/docs/global.md b/docs/global.md new file mode 100644 index 0000000..b21dfc2 --- /dev/null +++ b/docs/global.md @@ -0,0 +1,26 @@ +# Global Settings + +There are two global settings + +1. Print information: default `true` +1. Auto Refresh: default `false` + +## Print information + +When this button is toggled, it will print information about the sequence imported by this addon, such as name of the object, into a separate file. + +The file has the naming pattern `bseq_{time}`, where `{time}` is the time when rendering is started. + +The file will be located in the [blender render output directory](https://docs.blender.org/manual/en/latest/editors/preferences/file_paths.html#render). [^1] + +![print](../images/print.png) + +[^1]: By default the value is `/tmp`, and this directory does not exit on windows system. So when the directory does not exist, it won't print information into file. + +## Auto Refresh + +When this button is toggled, it will [refresh](./list.md#refresh) **all the sequences whenever a frame change occurs**. + +This option can be useful when some of the sequences are imported while the data is still being generated and not yet complete. Refreshing all the sequences can detect the frames that were added after being initially imported. + +![auto refresh](../images/auto_refresh.png) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c3ca94a --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,35 @@ +.. blender-sequence-loader documentation master file, created by + sphinx-quickstart on Sun Oct 2 13:42:11 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to blender-sequence-loader's documentation! +=================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + about + build + usage + global + + list + settings + +.. toctree:: + :maxdepth: 2 + :caption: Advanced: + + format + frame + script + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/list.md b/docs/list.md new file mode 100644 index 0000000..73a8d50 --- /dev/null +++ b/docs/list.md @@ -0,0 +1,35 @@ +# List View + +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. + +Note: When rendering the animation, please turn on the [Lock Interface](https://docs.blender.org/manual/en/latest/interface/window_system/topbar.html?#render-menu)[^1]. This will prevent artifacts from occurring, especially if the user continues to operate the Blender interface during the render process. This is doubly relevant when using custom normals on meshes, as this might cause Blender to crash instead of just failing to load the correct geometry. + +![lock](../images/lock.png) + +[^1]: We have also had users stating that they are able to render perfectly well without enabling this setting, so you might be fine to disable this option if you need to. + +## Imported Sequence + +Here you can have an overview of all the sequences imported by this addon. When selecting a sequence, it will change the selected [active object](https://docs.blender.org/manual/en/latest/scene_layout/object/selecting.html#selections-and-the-active-object) as well. Vice versa, when the [active object](https://docs.blender.org/manual/en/latest/scene_layout/object/selecting.html#selections-and-the-active-object) changes, it will change the selection in this list view as well. + +![list](../images/list.png) + +## Enable & Disable + +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. + +To toggle an individual sequence, you can click on the `ENABLED` or `DISABLED` button in the list view. + +![enable](../images/enable.png) + +### Enable Selected & Disable Selected + +When you want to enable or disable multiple sequences, you can [select](https://docs.blender.org/manual/en/latest/scene_layout/object/selecting.html) multiple objects in the viewport, and then click `Enable Selected` or `Disable Selected` to enable/disable all selected objects. + +## Current Frame + +`Current Frame` shows the current frame of sequence being loaded. By default, the value is [blender current frame](https://docs.blender.org/manual/en/latest/editors/timeline.html#frame-controls). For advanced usage, you can refer [here](./frame.md). + +## Refresh + +`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. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..e149940 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +Sphinx==5.2.3 +sphinx_rtd_theme==1.0.0 +myst-parser==0.18.1 \ No newline at end of file diff --git a/docs/script.md b/docs/script.md new file mode 100644 index 0000000..2d66f0d --- /dev/null +++ b/docs/script.md @@ -0,0 +1,89 @@ +# Custom Script + +If you want to have your own way to import the mesh object, then you can write your own python script to read and import the mesh object. **Technically, you can write any python code here, so be careful of the risks, especially when executing unknown code.** + +A script is assigned on a **per object** basis, so you can have a different script for each object. + +## How to Enable it + +By default, this feature is turned off. You can enable it here by toggling the `Show Settings` in `Advanced` panel, then you can select the script you want to assign to this object. + +![script](../images/script.png) + +## Template + +We provide a simple template to show you how to use custom scripts. You can find the template as shown in the image. + +![template](../images/template.png) + +### template.py + +This one is the same as the default behavior of the addon. + +### dim3.py + +This template provides a way to import 3D volumetric meshes, particularly tetrahedra and hexahedron. + +The default behavior of the addon is that faces inside 3D meshes are discarded, since they are invisible in most cases. But sometimes, these inner faces can be useful, and you can use this addon to import these inner faces in a specific way. + +## Write Your Own Script + +If you want to write your own script, you only need to implement one of two methods. One is `preprocess`, another one is `process`. + +### Notes: + +There are many things to be careful with here: + +1. `process` has higher priority than `preprocess`, when `process` exist, `preprocess` will be ignored. +2. When neither of these two functions exist, the addon will use the default behavior. +3. If you write any other things, it will be ignored, such as import modules, e.g. `import numpy`, or write a helper function which you call inside of `process` or `preprocess`. +4. If you need to import modules, write it inside the `preprocess` or `process` function. For example + +```python +def preprocess(fileseq: fileseq.FileSequence, frame_number: int) -> meshio.Mesh: + import math + # math.sqrt(25) +``` + +5. These modules are available by default: `numpy`, `meshio`, `fileseq` +6. There is also a very useful convenience function available: + + +```python +def update_mesh(meshio_mesh: meshio.Mesh, mesh: bpy.types.Mesh): + # this function reads `meshio_mesh`, then write it into `mesh`, and old information of `mesh` will be lost. +``` + +### preprocess + +The function `preprocess` has the following signature + +```python +def preprocess(fileseq: fileseq.FileSequence, frame_number: int) -> meshio.Mesh: + pass +``` + +This function, takes 2 parameters +1. fileseq: the `filseq` object when imported +2. frame_number: blender current frame + +This function expects a return value of `meshio.Mesh` object, and then the addon will write this `meshio.Mesh` into Blender. For details about `meshio.Mesh` object, can be found [here](https://github.com/nschloe/meshio/wiki/meshio-Mesh()-data-structure). + +### process + +The function `preprocess` has the following signature + +```python +def process(fileseq: fileseq.FileSequence, frame_number: int, mesh: bpy.types.Mesh): + pass +``` + +This function, takes 3 parameters + +1. fileseq: the `filseq` object when imported +2. frame_number: blender current frame +3. mesh: [bpy.types.Mesh](https://docs.blender.org/api/current/bpy.types.Mesh.html#bpy.types.Mesh) object + +This function will directly read the file, then modify the `mesh` object, rather than constructing a `meshio.Mesh` object in between. It can be useful if `meshio.Mesh` is not versatile enough to hold the mesh information you want. + +But in general, it's much more complicated to construct the `bpy.types.Mesh` object, so we suggest that you use `preprocess` for the most cases, unless you really need `process` function. diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 0000000..a706fea --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,50 @@ +# Settings + +Here you can find settings for each sequence. + +## Sequence Information (read-only) + +This is **read-only** information to show the pattern of this sequence, and if it's using a relative path. + +![sequence_information](../images/sequence_information.png) + +## 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. The exact [geometry node](https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/index.html) setup can be seen in the [geometry nodes tab](https://docs.blender.org/manual/en/latest/editors/geometry_node.html) and may be modified as desired, e.g. to set the particle radius. + +When applying the `Point Cloud` geometry node, the vertices of the mesh are converted to a [Point Cloud](https://docs.blender.org/manual/en/latest/modeling/point_cloud.html), which can be rendered only by [Cycles](https://docs.blender.org/manual/en/latest/render/cycles/introduction.html) and only as spheres. + +When applying the `Instances` geometry node, 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). You are free to change instanced object in [Geometry Node Editor](https://docs.blender.org/manual/en/latest/editors/geometry_node.html). + +**CAUTION: Because this node setup relies on the [`Realize Instances`](https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/instances/realize_instances.html) node, the memory usage increases extremely rapidly. Make sure to save the `.blend` file before attempting this, as Blender may run out of memory!!! Depending on your hardware and instanced object, it may be safe to use as many as 100k particles.** + +Applying the `Mesh` geometry node will restore the default geometry nodes, which simply displays the imported geometry as it is. + +![geometry_nodes](../images/geometry_nodes.png) + +Notes: + +1. `Instances` is super memory hungry compared to `Point Cloud`. +2. After applying `Point Cloud` or `Instances` geometry nodes, you need to assign the material inside the geometry nodes, to be able to shade the object according to some imported field. 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`](https://docs.blender.org/manual/en/latest/render/shader_nodes/input/attribute.html) node in the [Shader Editor](https://docs.blender.org/manual/en/latest/editors/shader_editor.html) and simply specify the attribute string. The imported attributes can be seen in the [spreadsheet](https://docs.blender.org/manual/en/latest/editors/spreadsheet.html) browser of the Geometry Nodes tab and are also listed in the [addon UI](#attributes). + + +## Attributes + +This addon will also import attributes[^1] of the mesh object into the blender [attribute](https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/attributes_reference.html) system. + +Here it shows all the vertex attributes detected and imported. To avoid name collisions with [blender built-in attributes](https://docs.blender.org/manual/en/latest/modeling/geometry_nodes/attributes_reference.html#built-in-attributes), all names are renamed using `bseq_` as prefix. Names are read-only. For example, `id` -> `bseq_id`. Keep this in mind when accessing attributes in the shader editor. + +The value of the attributes can be viewed in blenders [spreadsheet](https://docs.blender.org/manual/en/latest/editors/spreadsheet.html) which is part of the geometry nodes tab. There are many ways to use these attributes, such as [attribute node](https://docs.blender.org/manual/en/latest/render/shader_nodes/input/attribute.html) when shading. + +![attribute](../images/attribute.png) + +[^1]: Vertex attributes only for now + +### 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). + +The button `Set selected as normal` will set current selected attribute as vertex normal[^2]. The button `Clear normal` will reset the vertex normal to use the default face normals. + +[^2]: 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 an integer instead of float, then Blender will simply give a runtime error. diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..663ce22 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,65 @@ +# Usage + +**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. + +The following clip shows the basic usage of the addon. In particular, it shows how to load and render a sequence of particles data. + +For the supported file formats refer to [here](./format.md). + +![usage](../images/usage.gif) + +## Access + +After installing addon, you can find it in the toolbar, which is accessible by clicking on the small arrow at the top right of the viewport or by pressing the `n` key on the keyboard. + +![drag](../images/drag.png) + +Then you can find it here. + +![location](../images/location.png) + +## Basic Import Settings + +### Directory + +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 to navigate to the directory and click "Accept". Files are shown but not selectable in this dialogue.** + +![directory](../images/directory.png) + +### Absolute vs. Relative Paths + +There is a small checkbox asking whether you want to use relative paths or not. + +When toggled **on**, the blender file **must be saved before loading the sequence**. Then this sequence will be loaded using the 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. + +If toggled **off (default)**, it will use the **absolute path to load the sequence**. For this, the `.blend` file does not have to be saved in advance. + +![path](../images/path.png) + +### File Sequences + +After selecting the directory, the addon will automatically detect all sequences in this directory, and automatically select the first one as the default value in `Sequence Pattern` box. If only one sequence exists, it will be used by default. When there are multiple patterns you can use the dropdown to select a different pattern. + +The sequences that can be detected usually have the format `{name}{frame_number}.{extension}`. For example, two files with names `Example0.obj`, `Example1.obj` can be detected as a sequence. For more details, you can check it in [fileseq](https://github.com/justinfx/fileseq) project. + +![sequence](../images/sequence.png) + +#### Custom Pattern + +Sometimes, the addon can't detect the sequences correctly, or there are too many sequences in this directory. Then you can manually type the sequence. + +First, enable the `Use Custom Pattern` button, then `Sequence Pattern` becomes to editable. + +![custom](../images/custom.png) + +The grammar for this sequence is to use a `@` or `#` as an indicator for a frame index. An example could be `example@.vtk`. For more details, you can check the [fileseq](https://github.com/justinfx/fileseq#check-a-directory-for-one-existing-sequence) project. + +## Edit Sequence + +Sometimes, if you want to keep the setup of the current blender file, such as name and materials, but you want to change the loaded files. You can do this by using `Edit Sequence Path`. + +First, you need to select the sequence you want to edit. You can select [**any of the objects imported by this addon**](./list.md). By default, the value is the [current active object](https://docs.blender.org/manual/en/latest/scene_layout/object/selecting.html#selections-and-the-active-object). If current object is not imported by this addon, such as a general cube, light, then it's the last active object imported by this addon. + +After clicking the `Edit Sequence Path`, the sequence information will be updated to the sequence provided in [Basic Import Settings](#basic-import-settings). + +![edit](../images/edit.png) 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/extern/fileseq b/extern/fileseq index 9ec0493..584ee22 160000 --- a/extern/fileseq +++ b/extern/fileseq @@ -1 +1 @@ -Subproject commit 9ec049373af37ec21a21d2a5564deb344a96f97f +Subproject commit 584ee2218b74a9c7f4127c922cc2c33dc5a706b4 diff --git a/extern/meshio b/extern/meshio index a6175e0..0138cc8 160000 --- a/extern/meshio +++ b/extern/meshio @@ -1 +1 @@ -Subproject commit a6175e0d9dfb2aa274392d1cd396e991f0487cbc +Subproject commit 0138cc8692b806b44b32d344f7961e8370121ff7 diff --git a/extern/python-future b/extern/python-future index 80523f3..af1db97 160000 --- a/extern/python-future +++ b/extern/python-future @@ -1 +1 @@ -Subproject commit 80523f383fbba1c6de0551e19d0277e73e69573c +Subproject commit af1db970b0879b59e7aeb798c27a623144561cff diff --git a/extern/rich b/extern/rich index 3734ff4..fd98182 160000 --- a/extern/rich +++ b/extern/rich @@ -1 +1 @@ -Subproject commit 3734ff45d7b30541aabdd656efb80c9896d445b3 +Subproject commit fd981823644ccf50d685ac9c0cfe8e1e56c9dd35 diff --git a/images/0.png b/images/0.png deleted file mode 100644 index 9277da2..0000000 Binary files a/images/0.png and /dev/null differ diff --git a/images/1.png b/images/1.png deleted file mode 100644 index e70076c..0000000 Binary files a/images/1.png and /dev/null differ diff --git a/images/2.png b/images/2.png deleted file mode 100644 index d2a6eb7..0000000 Binary files a/images/2.png and /dev/null differ diff --git a/images/3.png b/images/3.png deleted file mode 100644 index 8f2aa45..0000000 Binary files a/images/3.png and /dev/null differ diff --git a/images/4.png b/images/4.png deleted file mode 100644 index 8834678..0000000 Binary files a/images/4.png and /dev/null differ diff --git a/images/5.png b/images/5.png deleted file mode 100644 index f98e1ef..0000000 Binary files a/images/5.png and /dev/null differ diff --git a/images/7.png b/images/7.png deleted file mode 100644 index 0127f8f..0000000 Binary files a/images/7.png and /dev/null differ diff --git a/images/attribute.png b/images/attribute.png new file mode 100644 index 0000000..dff7f0a Binary files /dev/null and b/images/attribute.png differ diff --git a/images/auto_refresh.png b/images/auto_refresh.png new file mode 100644 index 0000000..a40d102 Binary files /dev/null and b/images/auto_refresh.png differ diff --git a/images/current_frame.png b/images/current_frame.png new file mode 100644 index 0000000..b8bc398 Binary files /dev/null and b/images/current_frame.png differ diff --git a/images/custom.png b/images/custom.png new file mode 100644 index 0000000..fe20858 Binary files /dev/null and b/images/custom.png differ diff --git a/images/directory.png b/images/directory.png new file mode 100644 index 0000000..e601c47 Binary files /dev/null and b/images/directory.png differ diff --git a/images/drag.png b/images/drag.png new file mode 100644 index 0000000..39fae42 Binary files /dev/null and b/images/drag.png differ diff --git a/images/edit.png b/images/edit.png new file mode 100644 index 0000000..866fa35 Binary files /dev/null and b/images/edit.png differ diff --git a/images/edit_driver.png b/images/edit_driver.png new file mode 100644 index 0000000..1646bf2 Binary files /dev/null and b/images/edit_driver.png differ diff --git a/images/enable.png b/images/enable.png new file mode 100644 index 0000000..439273e Binary files /dev/null and b/images/enable.png differ diff --git a/images/geometry_nodes.png b/images/geometry_nodes.png new file mode 100644 index 0000000..c731d2f Binary files /dev/null and b/images/geometry_nodes.png differ diff --git a/images/list.png b/images/list.png new file mode 100644 index 0000000..9e37f7f Binary files /dev/null and b/images/list.png differ diff --git a/images/location.png b/images/location.png new file mode 100644 index 0000000..8b148ab Binary files /dev/null and b/images/location.png differ diff --git a/images/6.png b/images/lock.png similarity index 100% rename from images/6.png rename to images/lock.png diff --git a/images/logo_as_path.svg b/images/logo_as_path.svg new file mode 100644 index 0000000..611f92e --- /dev/null +++ b/images/logo_as_path.svg @@ -0,0 +1,266 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/path.png b/images/path.png new file mode 100644 index 0000000..bd05287 Binary files /dev/null and b/images/path.png differ diff --git a/images/print.png b/images/print.png new file mode 100644 index 0000000..c1074ff Binary files /dev/null and b/images/print.png differ diff --git a/images/script.png b/images/script.png new file mode 100644 index 0000000..a167eec Binary files /dev/null and b/images/script.png differ diff --git a/images/sequence.png b/images/sequence.png new file mode 100644 index 0000000..173eca0 Binary files /dev/null and b/images/sequence.png differ diff --git a/images/sequence_information.png b/images/sequence_information.png new file mode 100644 index 0000000..3a59a24 Binary files /dev/null and b/images/sequence_information.png differ diff --git a/images/template.png b/images/template.png new file mode 100644 index 0000000..639895f Binary files /dev/null and b/images/template.png differ diff --git a/images/usage.gif b/images/usage.gif new file mode 100644 index 0000000..19a19b9 Binary files /dev/null and b/images/usage.gif differ diff --git a/simloader/__init__.py b/simloader/__init__.py deleted file mode 100644 index cdf071d..0000000 --- a/simloader/__init__.py +++ /dev/null @@ -1,64 +0,0 @@ -from .operators import SIMLOADER_OT_load, SIMLOADER_OT_edit, SIMLOADER_OT_resetpt, SIMLOADER_OT_resetmesh, SIMLOADER_OT_resetins,SIMLOADER_OT_set_as_split_norm,SIMLOADER_OT_remove_split_norm,SIMLOADER_OT_disable_selected,SIMLOADER_OT_enable_selected,SIMLOADER_OT_refresh_seq -from .properties import SIMLOADER_scene_property, SIMLOADER_obj_property,SIMLOADER_mesh_property -from .panels import SIMLOADER_UL_Obj_List, SIMLOADER_List_Panel, SIMLOADER_Settings, SIMLOADER_Import, SIMLOADER_Templates, SIMLOADER_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 datetime import datetime - - -def print_information(scene): - if not bpy.context.scene.SIMLOADER.print: - return - now = datetime.now() - path = bpy.context.scene.render.filepath - path = bpy.path.abspath(path) - filepath = path + '/simloader_' + now.strftime("%Y-%m-%d_%H-%M") - with open(filepath, 'w') as file: - file.write("Render Time: {}\n".format(now.strftime("%Y-%m-%d_%H-%M"))) - file.write("Simloader Objects in the scene:\n\n") - for obj in bpy.data.objects: - simloader_prop = obj.SIMLOADER - if simloader_prop.init: - file.write("Object name: {}\n".format(obj.name)) - file.write("Is it being animated: {}\n".format(simloader_prop.enabled)) - file.write("Filepath: {}\n".format(simloader_prop.pattern)) - file.write("Is it relative path: {}\n".format(simloader_prop.use_relative)) - file.write("\n\n") - - -@persistent -def SIMLOADER_initialize(scene): - if update_obj not in bpy.app.handlers.frame_change_post: - bpy.app.handlers.frame_change_post.append(update_obj) - subscribe_to_selected() - if print_information not in bpy.app.handlers.render_init: - bpy.app.handlers.render_init.append(print_information) - - -__all__ = [ - "SIMLOADER_OT_edit", - "SIMLOADER_OT_load", - "SIMLOADER_obj_property", - "SIMLOADER_initialize", - "SIMLOADER_Import", - "SIMLOADER_List_Panel", - "SIMLOADER_UL_Obj_List", - "SIMLOADER_scene_property", - "SIMLOADER_Templates", - "SIMLOADER_Settings", - "SIMLOADER_UL_Att_List", - "subscribe_to_selected", - "SIMLOADER_OT_resetpt", - "SIMLOADER_OT_resetmesh", - "SIMLOADER_OT_resetins", - "draw_template", - "unsubscribe_to_selected", - "SIMLOADER_OT_set_as_split_norm", - "SIMLOADER_mesh_property", - "SIMLOADER_OT_remove_split_norm", - "SIMLOADER_OT_disable_selected", - "SIMLOADER_OT_enable_selected", - "SIMLOADER_OT_refresh_seq", -] diff --git a/simloader/callback.py b/simloader/callback.py deleted file mode 100644 index 48a04fc..0000000 --- a/simloader/callback.py +++ /dev/null @@ -1,51 +0,0 @@ -import bpy -import fileseq - -# Code here are mostly about the callback/update/items functions used in properties.py - - -def update_path(self, context): - # When the path has been changed, reset the selected sequence to None - context.scene.SIMLOADER['fileseq'] = 1 - context.scene.SIMLOADER.use_pattern = False - context.scene.SIMLOADER.pattern = "" - - -def item_fileseq(self, context): - ''' - Detects all the file sequences in the directory - ''' - - p = context.scene.SIMLOADER.path - try: - f = fileseq.findSequencesOnDisk(p) - except: - return [("None", "No sequence detected", "", 1)] - - if not f: - return [("None", "No sequence detected", "", 1)] - file_seq = [] - if len(f) >= 20: - file_seq.append(("None", "Too much sequence detected, could be false detection, please use pattern below", "", 1)) - else: - count = 1 - for seq in f: - file_seq.append((str(seq), seq.basename() + "@" + seq.extension(), "", count)) - count += 1 - return file_seq - - -def update_selected_obj_num(self, context): - - # Here is when select sequences, then change the corresponding object to active object - index = context.scene.SIMLOADER.selected_obj_num - obj = bpy.data.objects[index] - - if context.scene.SIMLOADER.selected_obj_deselectall_flag: - bpy.ops.object.select_all(action="DESELECT") - obj.select_set(True) - context.view_layer.objects.active = obj - - -def poll_material(self, material): - return not material.is_grease_pencil \ No newline at end of file diff --git a/simloader/importer.py b/simloader/importer.py deleted file mode 100644 index 54f13cd..0000000 --- a/simloader/importer.py +++ /dev/null @@ -1,242 +0,0 @@ -import bpy -import meshio -import traceback -import fileseq -from .utils import show_message_box -import numpy as np -from mathutils import Matrix -import additional_file_formats - - -def extract_faces(cell: meshio.CellBlock): - if cell.type == "triangle": - return cell.data.astype(np.uint64) - elif cell.type == "triangle6": - pass - elif cell.type == "triangle7": - pass - elif cell.type == "quad": - return cell.data.astype(np.uint64) - elif cell.type == "quad8": - pass - elif cell.type == "quad9": - pass - elif cell.type == "tetra": - data = cell.data.astype(np.uint64) - faces = data[:, [0, 2, 1]] - faces = np.append(faces, data[:, [0, 3, 2]], axis=0) - faces = np.append(faces, data[:, [0, 1, 3]], axis=0) - faces = np.append(faces, data[:, [1, 2, 3]], axis=0) - faces_copy = np.copy(faces) - faces_copy.sort(axis=1) - _, indxs, count = np.unique(faces_copy, axis=0, return_index=True, return_counts=True) - faces = faces[indxs[count == 1]] - return faces - elif cell.type == "hexahedron": - data = cell.data.astype(np.uint64) - faces = data[:, [0, 3, 2, 1]] - faces = np.append(faces, data[:, [1, 5, 4, 0]], axis=0) - faces = np.append(faces, data[:, [4, 5, 6, 7]], axis=0) - faces = np.append(faces, data[:, [3, 7, 6, 2]], axis=0) - faces = np.append(faces, data[:, [1, 2, 6, 5]], axis=0) - faces = np.append(faces, data[:, [0, 4, 7, 3]], axis=0) - faces_copy = np.copy(faces) - faces_copy.sort(axis=1) - _, indxs, count = np.unique(faces_copy, axis=0, return_index=True, return_counts=True) - faces = faces[indxs[count == 1]] - return faces - elif cell.type == "vertex": - return np.array([]) - show_message_box(cell.type + " is unsupported mesh format yet") - return np.array([]) - - -def update_mesh(meshio_mesh, mesh): - # extract information from the meshio mesh - mesh_vertices = meshio_mesh.points - - n_poly = 0 - n_loop = 0 - n_verts = len(mesh_vertices) - - 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]) - if faces_loop_total.size > 0: - faces_loop_start = np.cumsum(faces_loop_total) - # Add a zero as first entry - faces_loop_start = np.roll(faces_loop_start, 1) - faces_loop_start[0] = 0 - - if len(mesh.vertices) == n_verts and len(mesh.polygons) == n_poly and len(mesh.loops) == n_loop: - pass - else: - mesh.clear_geometry() - mesh.vertices.add(n_verts) - mesh.loops.add(n_loop) - mesh.polygons.add(n_poly) - - mesh.vertices.foreach_set("co", mesh_vertices.ravel()) - 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) - mesh.polygons.foreach_set("use_smooth", [shade_scheme] * len(faces_loop_total)) - - mesh.update() - mesh.validate() - - # copy attributes - attributes = mesh.attributes - for k, v in meshio_mesh.point_data.items(): - k = "bseq_" + k - attribute = None - if k not in attributes: - if len(v.shape) == 1: - # one dimensional attribute - attribute = mesh.attributes.new(k, "FLOAT", "POINT") - if len(v.shape) == 2: - dim = v.shape[1] - if dim > 3: - show_message_box('higher than 3 dimensional attribue, ignored') - continue - if dim == 1: - attribute = mesh.attributes.new(k, "FLOAT", "POINT") - if dim == 2: - attribute = mesh.attributes.new(k, "FLOAT2", "POINT") - if dim == 3: - attribute = mesh.attributes.new(k, "FLOAT_VECTOR", "POINT") - if len(v.shape) > 2: - show_message_box('more than 2 dimensional tensor, ignored') - continue - else: - attribute = attributes[k] - name_string = None - if attribute.data_type == "FLOAT": - name_string = "value" - else: - name_string = 'vector' - - attribute.data.foreach_set(name_string, v.ravel()) - - # set as split norm - if mesh.SIMLOADER.split_norm_att_name and mesh.SIMLOADER.split_norm_att_name == k: - mesh.use_auto_smooth = True - mesh.normals_split_custom_set_from_vertices(v) - - -def create_obj(fileseq, use_relaitve, transform_matrix=Matrix([[1, 0, 0, 0], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])): - - current_frame = bpy.context.scene.frame_current - filepath = fileseq[current_frame % len(fileseq)] - - meshio_mesh = None - enabled = True - try: - ext = fileseq.extension().split('.')[-1] - if ext in additional_file_formats.additional_format_loader: - meshio_mesh = additional_file_formats.additional_format_loader[ext](filepath) - else: - meshio_mesh = meshio.read(filepath) - except Exception as e: - show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), - "Meshio Loading Error" + str(e), - icon="ERROR") - enabled = False - - # create the object - name = fileseq.basename() + "@" + fileseq.extension() - mesh = bpy.data.meshes.new(name) - object = bpy.data.objects.new(name, mesh) - object.SIMLOADER.use_relative = use_relaitve - if use_relaitve: - object.SIMLOADER.pattern = bpy.path.relpath(str(fileseq)) - else: - object.SIMLOADER.pattern = str(fileseq) - object.SIMLOADER.init = True - object.SIMLOADER.enabled = enabled - object.matrix_world = transform_matrix - if enabled: - update_mesh(meshio_mesh, object.data) - bpy.context.collection.objects.link(object) - bpy.ops.object.select_all(action="DESELECT") - bpy.context.view_layer.objects.active = object - - -def update_obj(scene, depsgraph=None): - # TODO if bpy in edit mode, then return - - current_frame = bpy.context.scene.frame_current - - for obj in bpy.data.objects: - if obj.SIMLOADER.init == False: - continue - if obj.SIMLOADER.enabled == False: - continue - - meshio_mesh = None - pattern = obj.SIMLOADER.pattern - if obj.SIMLOADER.use_relative: - pattern = bpy.path.abspath(pattern) - # in case the blender file was created on windows system, but opened in linux system - pattern = bpy.path.native_pathsep(pattern) - fs = fileseq.FileSequence(pattern) - - if obj.SIMLOADER.use_advance and obj.SIMLOADER.script_name: - script = bpy.data.texts[obj.SIMLOADER.script_name] - try: - exec(script.as_string()) - except Exception as e: - show_message_box(traceback.format_exc(), "running script: " + obj.SIMLOADER.script_name + " failed: " + str(e), - "ERROR") - continue - - if 'process' in locals(): - user_process = locals()['process'] - try: - user_process(fs, current_frame, obj.data) - except Exception as e: - show_message_box("Error when calling user process: " + traceback.format_exc(), icon="ERROR") - del locals()['process'] - # this continue means if process exist, all the remaining code will be ignored, whethere or not error occurs - continue - - elif 'preprocess' in locals(): - user_preprocess = locals()['preprocess'] - try: - meshio_mesh = user_preprocess(fs, current_frame) - except Exception as e: - show_message_box("Error when calling user preprocess: " + traceback.format_exc(), icon="ERROR") - # this continue means only if error occures, then goes to next bpy.object - continue - finally: - del locals()['preprocess'] - else: - filepath = fs[current_frame % len(fs)] - try: - ext = fs.extension().split('.')[-1] - if ext in additional_file_formats.additional_format_loader: - meshio_mesh = additional_file_formats.additional_format_loader[ext](filepath) - else: - meshio_mesh = meshio.read(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 not isinstance(meshio_mesh, meshio.Mesh): - show_message_box('function preprocess does not return meshio object', "ERROR") - continue - update_mesh(meshio_mesh, obj.data) diff --git a/simloader/messenger.py b/simloader/messenger.py deleted file mode 100644 index f9f35a3..0000000 --- a/simloader/messenger.py +++ /dev/null @@ -1,34 +0,0 @@ -import bpy - - -def selected_callback(): - if not bpy.context.view_layer.objects.active: - return - name = bpy.context.active_object.name - idx = bpy.data.objects.find(name) - if idx >= 0: - bpy.context.scene.SIMLOADER.selected_obj_deselectall_flag = False - bpy.context.scene.SIMLOADER.selected_obj_num = idx - bpy.context.scene.SIMLOADER.selected_obj_deselectall_flag = True - - -def subscribe_to_selected(): - import simloader - - # because current implementation may subscribe twice - # so clear once to avoid duplication - bpy.msgbus.clear_by_owner(simloader) - - bpy.msgbus.subscribe_rna( - key=(bpy.types.LayerObjects, 'active'), - # don't know why it needs this owner, so I set owner to this module `simloader` - owner=simloader, - # no args - args=(()), - notify=selected_callback, - ) - - -def unsubscribe_to_selected(): - import simloader - bpy.msgbus.clear_by_owner(simloader) diff --git a/simloader/operators.py b/simloader/operators.py deleted file mode 100644 index 290d4b4..0000000 --- a/simloader/operators.py +++ /dev/null @@ -1,282 +0,0 @@ -import bpy -import fileseq -from .messenger import * -import traceback -from .utils import show_message_box -from .importer import create_obj -import numpy as np - - -# Here are load and delete operations -class SIMLOADER_OT_load(bpy.types.Operator): - '''This operator loads a sequnce''' - bl_label = "Load Sequence" - bl_idname = "sequence.load" - bl_options = {"UNDO"} - - def execute(self, context): - scene = context.scene - importer_prop = scene.SIMLOADER - - if importer_prop.relative and not bpy.data.is_saved: - # use relative but file not saved - show_message_box("When using relative path, please save file before using it", icon="ERROR") - return {"CANCELLED"} - - fs = importer_prop.fileseq - use_pattern = importer_prop.use_pattern - - if not use_pattern and (not fs or fs == "None"): - # fs is none - return {'CANCELLED'} - if use_pattern: - if not importer_prop.pattern: - show_message_box("Pattern is empty", icon="ERROR") - return {"CANCELLED"} - fs = importer_prop.path + '/' + importer_prop.pattern - - try: - fs = fileseq.findSequenceOnDisk(fs) - except Exception as e: - show_message_box(traceback.format_exc(), "Can't find sequence: " + str(fs), "ERROR") - return {"CANCELLED"} - - create_obj(fs, importer_prop.relative) - return {"FINISHED"} - - -class SIMLOADER_OT_edit(bpy.types.Operator): - '''This operator changes a sequnce''' - bl_label = "Edit Sequences Path" - bl_idname = "sequence.edit" - bl_options = {"UNDO"} - - def execute(self, context): - scene = context.scene - importer_prop = scene.SIMLOADER - - if importer_prop.relative and not bpy.data.is_saved: - # use relative but file not saved - show_message_box("When using relative path, please save file before using it", icon="ERROR") - return {"CANCELLED"} - - fs = importer_prop.fileseq - use_pattern = importer_prop.use_pattern - - if not use_pattern and (not fs or fs == "None"): - # fs is none - return {'CANCELLED'} - if use_pattern: - if not importer_prop.pattern: - show_message_box("Pattern is empty", icon="ERROR") - return {"CANCELLED"} - fs = importer_prop.path + '/' + importer_prop.pattern - - try: - fs = fileseq.findSequenceOnDisk(fs) - except Exception as e: - show_message_box(traceback.format_exc(), "Can't find sequence: " + str(fs), "ERROR") - return {"CANCELLED"} - - sim_loader = context.scene.SIMLOADER - # it seems quite simple task, no need to create a function(for now) - if sim_loader.selected_obj_num >= len(bpy.data.objects): - return {"CANCELLED"} - obj = bpy.data.objects[sim_loader.selected_obj_num] - if importer_prop.relative: - obj.SIMLOADER.pattern = bpy.path.relpath(str(fs)) - else: - obj.SIMLOADER.pattern = str(fs) - obj.SIMLOADER.use_relative = importer_prop.relative - return {"FINISHED"} - - -class SIMLOADER_OT_resetpt(bpy.types.Operator): - '''This operator reset the geometry nodes of the sequence as a point cloud''' - bl_label = "Reset Geometry Nodes as Point Cloud" - bl_idname = "simloader.resetpt" - bl_options = {"UNDO"} - - def execute(self, context): - sim_loader = context.scene.SIMLOADER - obj = bpy.data.objects[sim_loader.selected_obj_num] - warn = False - for modifier in obj.modifiers: - if modifier.type == "NODES": - warn = True - obj.modifiers.remove(modifier) - if warn: - show_message_box("Exising geoemtry nodes of {} has been removed".format(obj.name), "Warning") - gn = obj.modifiers.new("SIMLOADER_GeometryNodse", "NODES") - # change starting from blender 3.2 - # https://developer.blender.org/rB08b4b657b64f - if bpy.app.version >= (3,2,0): - bpy.ops.node.new_geometry_node_group_assign() - gn.node_group.nodes.new('GeometryNodeMeshToPoints') - set_material = gn.node_group.nodes.new('GeometryNodeSetMaterial') - set_material.inputs[2].default_value = context.scene.SIMLOADER.material - - node0 = gn.node_group.nodes[0] - node1 = gn.node_group.nodes[1] - node2 = gn.node_group.nodes[2] - gn.node_group.links.new(node0.outputs[0], node2.inputs[0]) - gn.node_group.links.new(node2.outputs[0], set_material.inputs[0]) - gn.node_group.links.new(set_material.outputs[0], node1.inputs[0]) - bpy.ops.object.modifier_move_to_index(modifier=gn.name, index=0) - return {"FINISHED"} - - -class SIMLOADER_OT_resetmesh(bpy.types.Operator): - '''This operator reset the geometry nodes of the sequence as a point cloud''' - bl_label = "Reset Geometry Nodes as Mesh" - bl_idname = "simloader.resetmesh" - bl_options = {"UNDO"} - - def execute(self, context): - sim_loader = context.scene.SIMLOADER - obj = bpy.data.objects[sim_loader.selected_obj_num] - warn = False - for modifier in obj.modifiers: - if modifier.type == "NODES": - warn = True - obj.modifiers.remove(modifier) - if warn: - show_message_box("Exising geoemtry nodes of {} has been removed".format(obj.name), "Warning") - gn = obj.modifiers.new("SIMLOADER_GeometryNodse", "NODES") - # change starting from blender 3.2 - # https://developer.blender.org/rB08b4b657b64f - if bpy.app.version >= (3,2,0): - bpy.ops.node.new_geometry_node_group_assign() - bpy.ops.object.modifier_move_to_index(modifier=gn.name, index=0) - return {"FINISHED"} - - -class SIMLOADER_OT_resetins(bpy.types.Operator): - '''This operator reset the geometry nodes of the sequence as a point cloud''' - bl_label = "Reset Geometry Nodes as Instances" - bl_idname = "simloader.resetins" - bl_options = {"UNDO"} - - def execute(self, context): - sim_loader = context.scene.SIMLOADER - obj = bpy.data.objects[sim_loader.selected_obj_num] - warn = False - for modifier in obj.modifiers: - if modifier.type == "NODES": - warn = True - obj.modifiers.remove(modifier) - if warn: - show_message_box("Exising geoemtry nodes of {} has been removed".format(obj.name), "Warning") - gn = obj.modifiers.new("SIMLOADER_GeometryNodse", "NODES") - # change starting from blender 3.2 - # https://developer.blender.org/rB08b4b657b64f - if bpy.app.version >= (3,2,0): - bpy.ops.node.new_geometry_node_group_assign() - nodes = gn.node_group.nodes - links = gn.node_group.links - input_node = nodes[0] - output_node = nodes[1] - - instance_on_points = nodes.new('GeometryNodeInstanceOnPoints') - cube = nodes.new('GeometryNodeMeshCube') - realize_instance = nodes.new('GeometryNodeRealizeInstances') - set_material = nodes.new('GeometryNodeSetMaterial') - - instance_on_points.inputs['Scale'].default_value = [ - 0.05, - 0.05, - 0.05, - ] - set_material.inputs[2].default_value = context.scene.SIMLOADER.material - - links.new(input_node.outputs[0], instance_on_points.inputs['Points']) - links.new(cube.outputs[0], instance_on_points.inputs['Instance']) - links.new(instance_on_points.outputs[0], realize_instance.inputs[0]) - links.new(realize_instance.outputs[0], set_material.inputs[0]) - links.new(set_material.outputs[0], output_node.inputs[0]) - - bpy.ops.object.modifier_move_to_index(modifier=gn.name, index=0) - return {"FINISHED"} - - -class SIMLOADER_OT_set_as_split_norm(bpy.types.Operator): - '''This operator set the vertex attribute as vertex split normals''' - bl_label = "Set as split normal per Vertex" - bl_idname = "simloader.setsplitnorm" - bl_options = {"UNDO"} - - def execute(self, context): - sim_loader = context.scene.SIMLOADER - obj = bpy.data.objects[sim_loader.selected_obj_num] - mesh = obj.data - attr_index = sim_loader.selected_attribute_num - if attr_index >= len(mesh.attributes): - show_message_box("Please select the attribute") - return {"CANCELLED"} - mesh.SIMLOADER.split_norm_att_name = mesh.attributes[attr_index].name - - return {"FINISHED"} - - -class SIMLOADER_OT_remove_split_norm(bpy.types.Operator): - '''This operator remove the vertex attribute as vertex split normals''' - bl_label = "Remove split normal per Vertex" - bl_idname = "simloader.removesplitnorm" - bl_options = {"UNDO"} - - def execute(self, context): - sim_loader = context.scene.SIMLOADER - obj = bpy.data.objects[sim_loader.selected_obj_num] - mesh = obj.data - if mesh.SIMLOADER.split_norm_att_name: - mesh.SIMLOADER.split_norm_att_name = "" - - return {"FINISHED"} - - -class SIMLOADER_OT_disable_selected(bpy.types.Operator): - '''This operator disable all selected sequence''' - bl_label = "Disable Selected Sequence" - bl_idname = "simloader.disableselected" - bl_options = {"UNDO"} - - def execute(self, context): - for obj in bpy.context.selected_objects: - if obj.SIMLOADER.init and obj.SIMLOADER.enabled: - obj.SIMLOADER.enabled = False - return {"FINISHED"} - - -class SIMLOADER_OT_enable_selected(bpy.types.Operator): - '''This operator enable all selected sequence''' - bl_label = "Enable Selected Sequence" - bl_idname = "simloader.enableselected" - bl_options = {"UNDO"} - - def execute(self, context): - for obj in bpy.context.selected_objects: - if obj.SIMLOADER.init and not obj.SIMLOADER.enabled: - obj.SIMLOADER.enabled = True - return {"FINISHED"} - - -class SIMLOADER_OT_refresh_seq(bpy.types.Operator): - '''This operator refresh the sequence''' - bl_label = "Refresh Sequence" - bl_idname = "simloader.refresh" - - def execute(self, context): - scene = context.scene - obj = bpy.data.objects[scene.SIMLOADER.selected_obj_num] - - fs = obj.SIMLOADER.pattern - if obj.SIMLOADER.use_relative: - fs = bpy.path.abspath(fs) - fs = fileseq.findSequenceOnDisk(fs) - fs = fileseq.findSequenceOnDisk(fs.dirname() + fs.basename() + "@" + fs.extension()) - fs = str(fs) - if obj.SIMLOADER.use_relative: - fs = bpy.path.relpath(fs) - obj.SIMLOADER.pattern = fs - - return {"FINISHED"} diff --git a/simloader/panels.py b/simloader/panels.py deleted file mode 100644 index 55b3bdd..0000000 --- a/simloader/panels.py +++ /dev/null @@ -1,230 +0,0 @@ -import bpy -import os - - -class SIMLOADER_UL_Obj_List(bpy.types.UIList): - ''' - This controls the list of imported sequences. - ''' - - def filter_items(self, context, data, property): - objs = getattr(data, property) - flt_flags = [] - # not sure if I understand correctly about this - # see reference from https://docs.blender.org/api/current/bpy.types.UIList.html#advanced-uilist-example-filtering-and-reordering - for o in objs: - if o.SIMLOADER.init: - flt_flags.append(self.bitflag_filter_item) - else: - flt_flags.append(0) - flt_neworder = [] - return flt_flags, flt_neworder - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - row = layout.row() - row.prop(item, "name", text='Name ', emboss=False) - if item.SIMLOADER.enabled: - row.prop(item.SIMLOADER, "enabled", text = "ENABLED", icon="PLAY") - else: - row.prop(item.SIMLOADER, "enabled", text = "DISABLED", icon="PAUSE") - else: - # actually, I guess this line of code won't be executed? - layout.label(text="", translate=False, icon_value=icon) - - -class SIMLOADER_UL_Att_List(bpy.types.UIList): - ''' - This controls the list of attributes available for this sequence - ''' - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname): - if item: - layout.enabled = False - layout.prop(item, "name", text='Name ', emboss=False) - obj = bpy.data.objects[context.scene.SIMLOADER.selected_obj_num] - mesh = obj.data - if mesh.SIMLOADER.split_norm_att_name and mesh.SIMLOADER.split_norm_att_name == item.name: - layout.label(text="using as split norm") - - else: - # actually, I guess this line of code won't be executed? - layout.label(text="", translate=False, icon_value=icon) - - -class SIMLOADER_List_Panel(bpy.types.Panel): - ''' - This is the panel of imported sequences, bottom part of images/9.png - ''' - bl_label = "Imported Sequences" - bl_idname = "SIMLOADER_PT_list" - bl_space_type = 'VIEW_3D' - bl_region_type = "UI" - bl_category = "Sequence Loader" - bl_context = "objectmode" - - def draw(self, context): - layout = self.layout - sim_loader = context.scene.SIMLOADER - row = layout.row() - row.template_list("SIMLOADER_UL_Obj_List", "", bpy.data, "objects", sim_loader, "selected_obj_num", rows=2) - row = layout.row() - row.operator("simloader.enableselected", text="Enable Selected") - row.operator("simloader.disableselected", text="Disable Selected") - row = layout.row() - row.operator("sequence.edit", text="Edit Path") - row.operator("simloader.refresh", text="Refresh") - - -class SIMLOADER_Settings(bpy.types.Panel): - ''' - This is the panel of settings of selected sequence - ''' - bl_label = "Sequence Settings" - bl_idname = "SIMLOADER_PT_settings" - bl_space_type = 'VIEW_3D' - bl_region_type = "UI" - bl_category = "Sequence Loader" - bl_context = "objectmode" - bl_options = {"DEFAULT_CLOSED"} - - def draw(self, context): - layout = self.layout - sim_loader = context.scene.SIMLOADER - if sim_loader.selected_obj_num >= len(bpy.data.objects): - return - obj = bpy.data.objects[sim_loader.selected_obj_num] - if not obj.SIMLOADER.init: - return - - # path settings - layout.label(text="Sequence Information (read-only)") - box = layout.box() - - split = box.split() - col1 = split.column() - col1.alignment = 'RIGHT' - col2 = split.column(align=False) - - col2.enabled = False - col1.label(text='Relative') - col2.prop(obj.SIMLOADER, 'use_relative', text="") - col1.label(text='Pattern') - col2.prop(obj.SIMLOADER, 'pattern', text="") - - # geometry nodes settings - layout.label(text="Geometry Nodes") - box = layout.box() - box.label(text="Point Cloud and Instances Material") - split = box.split() - col1 = split.column() - col1.alignment = 'RIGHT' - col2 = split.column() - col1.label(text="Material") - col2.prop_search(sim_loader, 'material', bpy.data, 'materials', text="") - box.label(text='Reset Geometry Nodes to') - - split = box.split() - col1 = split.column() - col2 = split.column() - col3 = split.column() - col1.operator('SIMLOADER.resetpt', text="Point Cloud") - col2.operator('SIMLOADER.resetmesh', text="Mesh") - col3.operator('SIMLOADER.resetins', text="Instances") - - - # attributes settings - layout.label(text="Attributes") - box = layout.box() - row = box.row() - row.template_list("SIMLOADER_UL_Att_List", "", obj.data, "attributes", sim_loader, "selected_attribute_num", rows=2) - box.operator("SIMLOADER.setsplitnorm", text="Set selected as normal") - box.operator("SIMLOADER.removesplitnorm", text="Clear normal") - - # advance settings - layout.label(text="Advanced") - box = layout.box() - split = box.split() - col1 = split.column() - col1.alignment = 'RIGHT' - col2 = split.column(align=False) - col1.label(text="Show Settings") - col2.prop(obj.SIMLOADER, 'use_advance', text="") - if obj.SIMLOADER.use_advance: - col1.label(text='Script') - col2.prop_search(obj.SIMLOADER, 'script_name', bpy.data, 'texts', text="") - - -class SIMLOADER_Import(bpy.types.Panel): - ''' - This is the panel of main addon interface. see images/1.jpg - ''' - bl_label = "Sequence Loader" - bl_idname = "SIMLOADER_PT_panel" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "Sequence Loader" - bl_context = "objectmode" - - def draw(self, context): - layout = self.layout - scene = context.scene - importer_prop = scene.SIMLOADER - - layout.label(text="Basic Import Settings") - box = layout.box() - split = box.split() - col1 = split.column() - col1.alignment = 'RIGHT' - col2 = split.column(align=False) - - col1.label(text="Directory") - col2.prop(importer_prop, "path", text="") - - col1.label(text="Use Custom Pattern") - col2.prop(importer_prop, "use_pattern", text="") - col1.label(text="Sequence Pattern") - if importer_prop.use_pattern: - col2.prop(importer_prop, "pattern", text="") - else: - col2.prop(importer_prop, "fileseq", text="") - - col1.label(text="Use Relative Path") - col2.prop(importer_prop, "relative", text="") - - layout.operator("sequence.load") - - layout.label(text="Global Settings") - box = layout.box() - split = box.split() - col1 = split.column() - col1.alignment = 'RIGHT' - col2 = split.column(align=False) - - col1.label(text="Print Sequence Information on Render") - col2.prop(importer_prop, "print", text="") - - -class SIMLOADER_Templates(bpy.types.Menu): - ''' - Here is the template panel, shown in the text editor -> templates - ''' - bl_label = "Sequence Loader" - bl_idname = "SIMLOADER_MT_template" - - def draw(self, context): - current_folder = os.path.dirname(os.path.abspath(__file__)) - self.path_menu( - # it goes to current folder -> parent folder -> template folder - [current_folder + '/../template'], - "text.open", - props_default={"internal": True}, - ) - - -def draw_template(self, context): - ''' - Here it function call to integrate template panel into blender template interface - ''' - layout = self.layout - layout.menu(SIMLOADER_Templates.bl_idname) \ No newline at end of file diff --git a/simloader/properties.py b/simloader/properties.py deleted file mode 100644 index dbf5911..0000000 --- a/simloader/properties.py +++ /dev/null @@ -1,52 +0,0 @@ -import bpy -from .callback import * - - -class SIMLOADER_scene_property(bpy.types.PropertyGroup): - path: bpy.props.StringProperty(name="Directory", - subtype="DIR_PATH", - description="You need to go to the folder with the sequence, then click \"Accept\". ", - update=update_path) - relative: bpy.props.BoolProperty(name='Use relative path', description="whether or not to use reletive path", default=False) - fileseq: bpy.props.EnumProperty( - name="File Sequences", - description="Please choose the file sequences you want", - items=item_fileseq, - ) - use_pattern: bpy.props.BoolProperty(name='Use pattern', - description="whether or not to use manually typed pattern", - default=False) - pattern: bpy.props.StringProperty(name="Pattern", - description="You can specify the pattern here, in case the sequence can't be deteced.") - - selected_obj_deselectall_flag: bpy.props.BoolProperty(default=True, - description="the flag to determine whether call deselect all or not ") - selected_obj_num: bpy.props.IntProperty(name='imported count', - description='the number of imported sequence, when selecting from ui list', - default=0, - update=update_selected_obj_num) - selected_attribute_num: bpy.props.IntProperty(default=0) - - material: bpy.props.PointerProperty( - type=bpy.types.Material, - poll=poll_material, - ) - - print: bpy.props.BoolProperty(name='print', - description="whether or not to print additional information when rendering", - default=True) - - -class SIMLOADER_obj_property(bpy.types.PropertyGroup): - init: bpy.props.BoolProperty(default=False) - enabled: bpy.props.BoolProperty(default=True, - description="When disbaled, the sequence won't be updated at each frame. Enabled by default") - use_advance: bpy.props.BoolProperty(default=False) - script_name: bpy.props.StringProperty() - use_relative: bpy.props.BoolProperty(default=False) - pattern: bpy.props.StringProperty() - - -# set this property for mesh, not object (maybe change later?) -class SIMLOADER_mesh_property(bpy.types.PropertyGroup): - split_norm_att_name: bpy.props.StringProperty(default="") diff --git a/simloader/utils.py b/simloader/utils.py deleted file mode 100644 index c45027f..0000000 --- a/simloader/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -import bpy - - -def show_message_box(message="", title="Message Box", icon="INFO"): - ''' - It shows a small window to display the error message and also print it the console - ''' - - def draw(self, context): - lines = message.splitlines() - for line in lines: - self.layout.label(text=line) - - print("Information: ", title) - print(message) - print('End of simloader message box') - print() - 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 - bpy.ops.screen.animation_cancel() - 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 diff --git a/template/dim3.py b/template/dim3.py new file mode 100644 index 0000000..c58ab70 --- /dev/null +++ b/template/dim3.py @@ -0,0 +1,36 @@ +# Here is an template to load 3-d mesh +# By default, the addon only renders the surface faces +# The template here will render all the faces, including the fase inside the mesh + +# NOTE: this might break the `shade smooth` in blender +import fileseq +import meshio +import numpy as np + + +def preprocess(fileseq: fileseq.FileSequence, frame_number: int) -> meshio.Mesh: + frame_number = frame_number % len(fileseq) + mesh = meshio.read(fileseq[frame_number]) + new_cells = [] + for cell in mesh.cells: + if cell.type == "tetra": + faces = [] + for d in cell.data: + faces.append([d[1], d[0], d[2]]) + faces.append([d[0], d[1], d[3]]) + faces.append([d[0], d[3], d[2]]) + faces.append([d[1], d[2], d[3]]) + new_cells.append(('triangle', np.array(faces, dtype=np.uint64))) + elif cell.type == "hexahedron": + faces = [] + for d in cell.data: + faces.append([d[0], d[3], d[2], d[1]]) + faces.append([d[1], d[2], d[6], d[5]]) + faces.append([d[1], d[5], d[4], d[0]]) + faces.append([d[4], d[5], d[6], d[7]]) + faces.append([d[2], d[3], d[7], d[6]]) + faces.append([d[0], d[4], d[7], d[3]]) + new_cells.append(('quad', np.array(faces, dtype=np.uint64))) + else: + new_cells.append((cell.type, cell.data)) + return meshio.Mesh(mesh.points, new_cells, mesh.point_data) diff --git a/template/dim4.py b/template/dim4.py deleted file mode 100644 index ed00217..0000000 --- a/template/dim4.py +++ /dev/null @@ -1,22 +0,0 @@ -import fileseq -import meshio -import numpy as np - -def preprocess(fileseq: fileseq.FileSequence, frame_number: int) -> meshio.Mesh: - # this renders all the faces(both surface and inside) - # by default, the addon only renders the surface faces - frame_number = frame_number % len(fileseq) - mesh = meshio.read(fileseq[frame_number]) - new_cells = [] - for cell in mesh.cells: - if cell.type=="tetra": - faces = [] - for d in cell.data: - faces.append([d[0],d[1],d[2]]) - faces.append([d[0],d[1],d[3]]) - faces.append([d[0],d[2],d[3]]) - faces.append([d[1],d[2],d[3]]) - new_cells.append(('triangle',np.array(faces, dtype=np.int32))) - else: - new_cells.append((cell.type,cell.data)) - return meshio.Mesh(mesh.points,new_cells,mesh.point_data) \ No newline at end of file