diff --git a/README.md b/README.md index 93468d8..7a3930a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ GoZ-alike tools for simple ZBrush<->Blender interchange. +This is a fork of [JoseConseco/GoB](https://github.com/JoseConseco/GoB). Upstream docs and the wiki live there; this repo carries additional import fixes and improvements. + ## Features You can transfer: * Objects (only meshes) @@ -14,23 +16,34 @@ You can transfer: * Normal map * Displacement map -## GoB Setup -1. Download the latest "Source code (zip)" from "Releases" -2. Open Blender and Navigate to Edit > Preferences > Get Extensions -3. Uninstall any previous version of GoB by locating it in your extensions list, opening the drop down menu on the right side, and clicking "uninstall". -4. Install GoB by clicking the drop down menu found in the top right of the extensions window, and selecting "install from disk". -5. Navigate to and select the zip file downloaded in step One. -6. Locate GoB in Edit > Preferences > Add-ons to configure your settings. +## Fork changes + +Import reliability fixes on top of upstream: +* **View layer re-linking** — Re-importing an object that already exists in the `.blend` but is not on the active view layer (orphaned or in an excluded collection) no longer crashes on `select_set()`. The importer re-links the object into the scene collection and view layer before selecting it. +* **Low-poly name resolution** — ZBrush `_low2` / `_low3` object names are mapped onto the canonical Blender `_low` mesh when present. +* **Collection sync** — Imported `_low` meshes are linked into the same collections as their matching `_high` object. +* **Stale variant cleanup** — Duplicate numbered low variants (for example `_low2`, `_low.001`) are removed after resolving to the canonical `_low` object. +* **Face set safety** — Polygroup / face-set data is trimmed or padded to match the imported face count when counts diverge. + +## GoB Setup +1. Download the latest **Source code (zip)** from [Releases](https://github.com/combwizard/GoB/releases), or use **Code → Download ZIP** on the branch you want to install. +2. Open Blender and navigate to **Edit → Preferences → Get Extensions**. +3. Uninstall any previous version of GoB by locating it in your extensions list, opening the drop-down menu on the right, and clicking **Uninstall**. +4. Install GoB by clicking the drop-down menu in the top right of the extensions window and selecting **Install from Disk**. +5. Select the zip file downloaded in step 1. +6. Locate GoB under **Edit → Preferences → Add-ons** to configure your settings. ## Usage -The addon adds two icons Import/Export to the top info panel: -* By clicking on the Export icon, you export the selected mesh objects into ZBrush. -* By clicking on the Import icon, you toggle autoloading mode. This will automatically load any models into blender that are exported from ZBrush via GoZ. -* By clicking on the Manual icon, you execute a one time import of the most recent model exported from ZBrush via GoZ. +The addon adds three buttons to the top info panel: +* **Export** — Export the selected mesh objects to ZBrush. +* **Import** — Toggle autoloading mode. Models exported from ZBrush via GoZ are loaded into Blender automatically while this mode is on. +* **Manual** — Run a one-time import of the most recent model exported from ZBrush via GoZ. -# Acknowledgements +## Acknowledgements This script was originally written by user "Stunton" and posted [here on ZBrushCentral](http://www.zbrushcentral.com/showthread.php?127419-GoB-an-unofficial-GoZ-for-Blender). It was also [posted on Blender's wiki](https://en.blender.org/index.php/Extensions:2.6/Py/Scripts/Import-Export/GoB_ZBrush_import_export) in the Import/Export Addons category, with the author listed as "ODe". + +Maintained upstream by JoseConseco, Daniel Grauer (kromar), and contributors. Fork maintained by [combwizard](https://github.com/combwizard). diff --git a/gob_import.py b/gob_import.py index 1eb0fc2..1c6d5f0 100644 --- a/gob_import.py +++ b/gob_import.py @@ -18,6 +18,7 @@ import os import random +import re import string import time from struct import unpack @@ -48,6 +49,99 @@ class GoB_OT_import(Operator): ] ) + def _resolve_import_object(self, obj_name: str): + """Map ZBrush _low2 / _low3 names onto the canonical Blender _low object.""" + obj = bpy.data.objects.get(obj_name) + if obj is not None: + return obj, obj_name + + match = re.match(r"^(?P.+)_low\d+$", obj_name) + if match: + canonical = f"{match.group('base')}_low" + obj = bpy.data.objects.get(canonical) + if obj is not None and obj.type == "MESH": + if utils.prefs().debug_output: + print(f"GoB: resolved import name {obj_name} → {canonical}") + return obj, canonical + + return None, obj_name + + def _high_name_for_low(self, low_name: str) -> str | None: + if not low_name.lower().endswith("_low"): + return None + high_name = re.sub(r"_low$", "_high", low_name, flags=re.IGNORECASE) + high_obj = bpy.data.objects.get(high_name) + if high_obj is not None and high_obj.type == "MESH": + return high_name + return None + + def _sync_low_collections_from_high(self, obj: bpy.types.Object, low_name: str) -> None: + high_name = self._high_name_for_low(low_name) + if high_name is None: + return + high_obj = bpy.data.objects.get(high_name) + if high_obj is None: + return + + scene_root = bpy.context.scene.collection + for coll in high_obj.users_collection: + if obj.name not in coll.objects: + coll.objects.link(obj) + + if scene_root not in high_obj.users_collection and obj.name in scene_root.objects: + try: + scene_root.objects.unlink(obj) + except RuntimeError: + pass + + def _remove_stale_low_variants(self, canonical_obj: bpy.types.Object, low_name: str) -> None: + if not low_name.lower().endswith("_low"): + return + base = low_name[: -len("_low")] + prefix = f"{base}_low" + for other in list(bpy.data.objects): + if other == canonical_obj or other.type != "MESH": + continue + if not other.name.startswith(prefix): + continue + suffix = other.name[len(prefix) :] + if suffix.isdigit() or suffix.startswith("."): + if utils.prefs().debug_output: + print(f"GoB: removing stale low variant {other.name}") + bpy.data.objects.remove(other, do_unlink=True) + + def _ensure_object_in_view_layer(self, obj: bpy.types.Object, obj_name: str) -> None: + """Link obj into the active view layer if it only exists as orphan/excluded data.""" + view_layer = bpy.context.view_layer + if obj.name in view_layer.objects: + return + + if utils.prefs().debug_output: + print(f"\nGoB Re-linking object into view layer: {obj_name}") + + # Scene root is always on the view layer; active subcollections may be excluded. + scene_coll = bpy.context.scene.collection + if obj.name not in scene_coll.objects: + scene_coll.objects.link(obj) + + obj.hide_set(False) + obj.hide_viewport = False + view_layer.update() + + if obj.name not in view_layer.objects: + for coll in list(obj.users_collection): + if coll != scene_coll: + coll.objects.unlink(obj) + if obj.name not in scene_coll.objects: + scene_coll.objects.link(obj) + view_layer.update() + + if obj.name not in view_layer.objects: + raise RuntimeError( + f"GoB: '{obj_name}' could not be added to view layer " + f"'{view_layer.name}'" + ) + def make_mesh(self, objName, vertsData, facesData) -> tuple: """Create or update a mesh object from the given vertices and faces data. @@ -63,7 +157,8 @@ def make_mesh(self, objName, vertsData, facesData) -> tuple: if utils.prefs().debug_output: print(f"\nGoB Object Name: {objName}") - obj = bpy.data.objects.get(objName) + goz_name = objName + obj, objName = self._resolve_import_object(objName) if obj: if utils.prefs().debug_output: print(f"\nGoB Object already exists: {objName}") @@ -73,14 +168,8 @@ def make_mesh(self, objName, vertsData, facesData) -> tuple: print(f"\nGoB Creating new object: {objName}") me = bpy.data.meshes.new(objName) obj = bpy.data.objects.new(objName, me) - if bpy.context.view_layer.active_layer_collection: - bpy.context.view_layer.active_layer_collection.collection.objects.link( - obj - ) - else: - print( - "Error: Active layer collection is not set or invalid. Object could not be linked." - ) + + self._ensure_object_in_view_layer(obj, objName) # Clear and update mesh geometry if bpy.app.version >= (3, 6, 0): @@ -99,6 +188,10 @@ def make_mesh(self, objName, vertsData, facesData) -> tuple: me.validate(verbose=utils.prefs().debug_output) # Set object as active and update view layer + self._ensure_object_in_view_layer(obj, objName) + self._sync_low_collections_from_high(obj, objName) + if goz_name != objName: + self._remove_stale_low_variants(obj, objName) obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.context.view_layer.update() @@ -549,8 +642,12 @@ def GoZit(self, pathFile): obj.data.attributes.new(".sculpt_face_set", "INT", "FACE") face_set_index_storage = [int(pgmat) for pgmat in polyGroupData] + face_count = len(obj.data.polygons) + # Assign data to polygons for i, pgmat in enumerate(polyGroupData): + if i >= face_count: + break if utils.prefs().import_material == "POLYGROUPS": obj.data.polygons[i].material_index = obj.material_slots[ str(pgmat) @@ -564,6 +661,21 @@ def GoZit(self, pathFile): # Apply face sets if utils.prefs().import_polygroups_to_facesets: + if len(face_set_index_storage) != face_count: + if utils.prefs().debug_output: + print( + "GoB: polygroup count", + len(face_set_index_storage), + "!= face count", + face_count, + "- adjusting face sets", + ) + if len(face_set_index_storage) > face_count: + face_set_index_storage = face_set_index_storage[:face_count] + else: + face_set_index_storage.extend( + [0] * (face_count - len(face_set_index_storage)) + ) obj.data.attributes[".sculpt_face_set"].data.foreach_set( "value", face_set_index_storage )