Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 25 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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).
130 changes: 121 additions & 9 deletions gob_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import os
import random
import re
import string
import time
from struct import unpack
Expand Down Expand Up @@ -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<base>.+)_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.

Expand All @@ -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}")
Expand All @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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
)
Expand Down