This commit is contained in:
Peterino2 2026-07-10 23:02:44 -07:00
parent ca9ddb196e
commit ef7fea6252
6 changed files with 250 additions and 7 deletions

View File

@ -22,11 +22,33 @@ Build every Blender extension in the repository at once with:
Set `BLENDER_BIN=/path/to/blender` when Blender is not available as `blender`. Set `BLENDER_BIN=/path/to/blender` when Blender is not available as `blender`.
Build, install, and enable every extension headlessly for the current Blender user with:
```bash
./install-all.sh
```
The installer targets Blender's `user_default` extension repository. Override it with `BLENDER_EXTENSION_REPO=repository_id` when needed. Close running Blender instances before installing so they do not retain an older loaded copy.
On Windows, use the equivalent batch file from Command Prompt:
```bat
install-all.bat
```
If Blender is not on `PATH`, configure it without embedding extra quotes:
```bat
set "BLENDER_BIN=C:\Program Files\Blender Foundation\Blender 5.2\blender.exe"
set "BLENDER_EXTENSION_REPO=user_default"
install-all.bat
```
## Material ID Baker extension ## Material ID Baker extension
The Blender extension bakes the active mesh's material-slot assignments to a flat-color texture using its active UV map. It works on a temporary mesh copy, so the source mesh and materials are not modified. The Blender extension bakes the active mesh's material-slot assignments to a flat-color texture using its active UV map. It works on a temporary mesh copy, so the source mesh and materials are not modified.
Features include 256px8K output, configurable island margin, optional evaluated modifiers, and three palette modes: deterministic distinct colors, material viewport colors, and exact slot-index encoding. It can write a PNG plus a JSON legend used by the standalone merger. Features include 256px8K output, configurable island margin, optional evaluated modifiers, and three palette modes: deterministic distinct colors, material viewport colors, and exact slot-index encoding. It can write a PNG plus a JSON legend used by the standalone merger. The optional **Create Export Copy** setting leaves behind a selected duplicate with one `baked ids` material connected to the baked image, ready to export as a single-material asset.
### Install or update ### Install or update
@ -46,7 +68,8 @@ You can instead run `./blender/scripts/build.sh` and install the resulting ZIP t
2. Ensure it has an active UV map and the intended per-face material assignments. 2. Ensure it has an active UV map and the intended per-face material assignments.
3. Open the 3D Viewport sidebar and choose the **Material ID** tab. 3. Open the 3D Viewport sidebar and choose the **Material ID** tab.
4. Select the resolution, margin, and color mode. 4. Select the resolution, margin, and color mode.
5. Enable **Save PNG** and **Save JSON Legend**, then click **Bake Material ID**. 5. Optionally enable **Create Export Copy** to create a one-material duplicate for export.
6. Enable **Save PNG** and **Save JSON Legend**, then click **Bake Material ID**.
Overlapping UV islands with different materials are ambiguous. Use a non-overlapping UV layout for recovery/compositing. Overlapping UV islands with different materials are ambiguous. Use a non-overlapping UV layout for recovery/compositing.

View File

@ -78,6 +78,66 @@ def _create_id_material(
return material return material
def _create_baked_ids_material(image: bpy.types.Image) -> bpy.types.Material:
"""Create an export-friendly, single material displaying the baked ID image."""
material = bpy.data.materials.new("baked ids")
material.use_nodes = True
nodes = material.node_tree.nodes
nodes.clear()
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (560.0, 0.0)
principled = nodes.new("ShaderNodeBsdfPrincipled")
principled.location = (260.0, 0.0)
principled.inputs["Roughness"].default_value = 1.0
image_node = nodes.new("ShaderNodeTexImage")
image_node.label = "Baked Material IDs"
image_node.location = (-120.0, 0.0)
image_node.image = image
image_node.interpolation = "Closest"
nodes.active = image_node
material.node_tree.links.new(image_node.outputs["Color"], principled.inputs["Base Color"])
material.node_tree.links.new(principled.outputs["BSDF"], output.inputs["Surface"])
return material
def _create_export_copy(
source: bpy.types.Object,
baked_mesh: bpy.types.Mesh,
image: bpy.types.Image,
apply_modifiers: bool,
scene: bpy.types.Scene,
) -> tuple[bpy.types.Object, bpy.types.Mesh, bpy.types.Material]:
"""Create a persistent one-material copy suitable for later export."""
if apply_modifiers:
export_mesh = baked_mesh.copy()
export_object = bpy.data.objects.new(
f"{source.name}_Baked_IDs",
export_mesh,
)
export_object.matrix_world = source.matrix_world.copy()
else:
export_object = source.copy()
export_mesh = source.data.copy()
export_object.data = export_mesh
export_object.name = f"{source.name}_Baked_IDs"
target_collection = (
source.users_collection[0] if source.users_collection else scene.collection
)
target_collection.objects.link(export_object)
material = _create_baked_ids_material(image)
export_mesh.materials.clear()
export_mesh.materials.append(material)
for polygon in export_mesh.polygons:
polygon.material_index = 0
return export_object, export_mesh, material
def _normalise_png_path(filepath: str) -> Path: def _normalise_png_path(filepath: str) -> Path:
path = Path(bpy.path.abspath(filepath)).expanduser() path = Path(bpy.path.abspath(filepath)).expanduser()
if path.suffix.lower() != ".png": if path.suffix.lower() != ".png":
@ -164,6 +224,13 @@ class MIDB_Settings(PropertyGroup):
description="Bake evaluated modifier geometry instead of the base mesh", description="Bake evaluated modifier geometry instead of the base mesh",
default=False, default=False,
) )
create_export_copy: BoolProperty(
name="Create Export Copy",
description=(
"Create and select a duplicate with one 'baked ids' material using the baked image"
),
default=False,
)
image_name: StringProperty( image_name: StringProperty(
name="Image Name", name="Image Name",
default="Material_ID", default="Material_ID",
@ -226,6 +293,9 @@ class MIDB_OT_Bake(Operator):
temp_mesh: bpy.types.Mesh | None = None temp_mesh: bpy.types.Mesh | None = None
temp_materials: list[bpy.types.Material] = [] temp_materials: list[bpy.types.Material] = []
image: bpy.types.Image | None = None image: bpy.types.Image | None = None
export_object: bpy.types.Object | None = None
export_mesh: bpy.types.Mesh | None = None
export_material: bpy.types.Material | None = None
succeeded = False succeeded = False
try: try:
@ -372,11 +442,23 @@ class MIDB_OT_Bake(Operator):
entry.material_name = item["material"] entry.material_name = item["material"]
entry.color = item["rgb"] entry.color = item["rgb"]
if settings.create_export_copy:
export_object, export_mesh, export_material = _create_export_copy(
source,
temp_mesh,
image,
settings.apply_modifiers,
scene,
)
succeeded = True succeeded = True
if saved_path: if saved_path:
self.report({"INFO"}, f"Baked material IDs to {saved_path}") message = f"Baked material IDs to {saved_path}"
else: else:
self.report({"INFO"}, f"Baked material IDs to image '{image.name}'") message = f"Baked material IDs to image '{image.name}'"
if export_object is not None:
message += f" and created '{export_object.name}'"
self.report({"INFO"}, message)
return {"FINISHED"} return {"FINISHED"}
except Exception as exc: except Exception as exc:
@ -392,6 +474,13 @@ class MIDB_OT_Bake(Operator):
if material.users == 0: if material.users == 0:
bpy.data.materials.remove(material) bpy.data.materials.remove(material)
if not succeeded and export_object is not None:
bpy.data.objects.remove(export_object, do_unlink=True)
if not succeeded and export_mesh is not None and export_mesh.users == 0:
bpy.data.meshes.remove(export_mesh)
if not succeeded and export_material is not None and export_material.users == 0:
bpy.data.materials.remove(export_material)
_restore_bake_settings(scene, bake_state) _restore_bake_settings(scene, bake_state)
for obj in list(context.selected_objects): for obj in list(context.selected_objects):
@ -402,6 +491,12 @@ class MIDB_OT_Bake(Operator):
if active_before and active_before.name in view_layer.objects: if active_before and active_before.name in view_layer.objects:
view_layer.objects.active = active_before view_layer.objects.active = active_before
if succeeded and export_object is not None:
for obj in list(context.selected_objects):
obj.select_set(False)
export_object.select_set(True)
view_layer.objects.active = export_object
if not succeeded and image is not None and image.users == 0: if not succeeded and image is not None and image.users == 0:
bpy.data.images.remove(image) bpy.data.images.remove(image)
@ -436,6 +531,7 @@ class MIDB_PT_Panel(Panel):
column.prop(settings, "margin") column.prop(settings, "margin")
column.prop(settings, "palette_mode") column.prop(settings, "palette_mode")
column.prop(settings, "apply_modifiers") column.prop(settings, "apply_modifiers")
column.prop(settings, "create_export_copy")
output = layout.box() output = layout.box()
output.label(text="Output") output.label(text="Output")

View File

@ -1,7 +1,7 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "material_id_baker" id = "material_id_baker"
version = "1.0.0" version = "1.1.0"
name = "Material ID Baker" name = "Material ID Baker"
tagline = "Bake mesh material assignments to a color ID texture" tagline = "Bake mesh material assignments to a color ID texture"
maintainer = "Sear" maintainer = "Sear"

View File

@ -79,6 +79,7 @@ def main() -> None:
settings.margin = 4 settings.margin = 4
settings.palette_mode = "DISTINCT" settings.palette_mode = "DISTINCT"
settings.apply_modifiers = True settings.apply_modifiers = True
settings.create_export_copy = True
settings.image_name = "Material_ID_Smoke" settings.image_name = "Material_ID_Smoke"
settings.save_to_disk = True settings.save_to_disk = True
settings.filepath = "/tmp/material_id_baker_smoke.png" settings.filepath = "/tmp/material_id_baker_smoke.png"
@ -86,13 +87,31 @@ def main() -> None:
result = bpy.ops.object.bake_material_id() result = bpy.ops.object.bake_material_id()
assert result == {"FINISHED"}, result assert result == {"FINISHED"}, result
assert bpy.context.active_object == source
assert source.select_get()
assert bpy.context.scene.render.engine == original_engine assert bpy.context.scene.render.engine == original_engine
assert not any(item.name.startswith("__MID_BAKER_") for item in bpy.data.objects) assert not any(item.name.startswith("__MID_BAKER_") for item in bpy.data.objects)
assert not any(item.name.startswith("__MID_BAKER_") for item in bpy.data.materials) assert not any(item.name.startswith("__MID_BAKER_") for item in bpy.data.materials)
export_copy = bpy.context.active_object
assert export_copy is not None
assert export_copy.name == "MaterialIDSmokeObject_Baked_IDs"
assert export_copy != source
assert export_copy.select_get()
assert not source.select_get()
assert len(export_copy.data.materials) == 1
baked_material = export_copy.data.materials[0]
assert baked_material.name == "baked ids"
assert all(polygon.material_index == 0 for polygon in export_copy.data.polygons)
assert len(export_copy.data.polygons) == 4
assert len(export_copy.modifiers) == 0
image = bpy.data.images[settings.last_image_name] image = bpy.data.images[settings.last_image_name]
image_nodes = [
node
for node in baked_material.node_tree.nodes
if node.bl_idname == "ShaderNodeTexImage"
]
assert len(image_nodes) == 1
assert image_nodes[0].image == image
palette = json.loads(image["material_id_palette"]) palette = json.loads(image["material_id_palette"])
assert len(palette) == 2 assert len(palette) == 2
assert palette[0]["material"] == "Left Material" assert palette[0]["material"] == "Left Material"

66
install-all.bat Normal file
View File

@ -0,0 +1,66 @@
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "REPO_ROOT=%~dp0"
set "BLENDER_ROOT=%REPO_ROOT%blender"
set "DIST_DIR=%REPO_ROOT%dist"
if not defined BLENDER_BIN set "BLENDER_BIN=blender"
if not defined BLENDER_EXTENSION_REPO set "BLENDER_EXTENSION_REPO=user_default"
if not exist "%BLENDER_ROOT%" (
echo No Blender extension directory found at "%BLENDER_ROOT%". 1>&2
exit /b 1
)
if not exist "%DIST_DIR%" mkdir "%DIST_DIR%"
if errorlevel 1 exit /b 1
set /a EXTENSION_COUNT=0
for /r "%BLENDER_ROOT%" %%F in (blender_manifest.toml) do (
set "EXTENSION_ID="
set "EXTENSION_VERSION="
for /f "tokens=1,* delims==" %%A in ('findstr /b /c:"id =" "%%F"') do set "EXTENSION_ID=%%B"
for /f "tokens=1,* delims==" %%A in ('findstr /b /c:"version =" "%%F"') do set "EXTENSION_VERSION=%%B"
set "EXTENSION_ID=!EXTENSION_ID: =!"
set "EXTENSION_ID=!EXTENSION_ID:"=!"
set "EXTENSION_VERSION=!EXTENSION_VERSION: =!"
set "EXTENSION_VERSION=!EXTENSION_VERSION:"=!"
if not defined EXTENSION_ID (
echo Could not read the extension id from "%%F". 1>&2
exit /b 1
)
if not defined EXTENSION_VERSION (
echo Could not read the extension version from "%%F". 1>&2
exit /b 1
)
for %%D in ("%%~dpF.") do set "SOURCE_DIR=%%~fD"
set "PACKAGE=%DIST_DIR%\!EXTENSION_ID!-!EXTENSION_VERSION!.zip"
echo Building Blender extension: !EXTENSION_ID! !EXTENSION_VERSION!
"%BLENDER_BIN%" --factory-startup --command extension build ^
--source-dir "!SOURCE_DIR!" ^
--output-filepath "!PACKAGE!"
if errorlevel 1 exit /b 1
echo Installing Blender extension: !PACKAGE!
"%BLENDER_BIN%" --factory-startup --command extension install-file ^
-r "%BLENDER_EXTENSION_REPO%" ^
-e "!PACKAGE!"
if errorlevel 1 exit /b 1
set /a EXTENSION_COUNT+=1
)
if !EXTENSION_COUNT! equ 0 (
echo No Blender extension manifests found under "%BLENDER_ROOT%". 1>&2
exit /b 1
)
echo Installed and enabled !EXTENSION_COUNT! Blender extension(s) in repository "%BLENDER_EXTENSION_REPO%".
exit /b 0

39
install-all.sh Executable file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
blender_bin="${BLENDER_BIN:-blender}"
repository="${BLENDER_EXTENSION_REPO:-user_default}"
build_marker="$(mktemp)"
cleanup() {
rm -f "$build_marker"
}
trap cleanup EXIT
"$repo_root/build-all.sh"
mapfile -d '' packages < <(
find "$repo_root/dist" \
-maxdepth 1 \
-type f \
-name '*.zip' \
-newer "$build_marker" \
-print0 \
| sort -z
)
if (( ${#packages[@]} == 0 )); then
echo "No Blender extension packages were produced by build-all.sh." >&2
exit 1
fi
for package in "${packages[@]}"; do
echo "Installing Blender extension: ${package#"$repo_root/"}"
"$blender_bin" --factory-startup --command extension install-file \
-r "$repository" \
-e "$package"
done
echo "Installed and enabled ${#packages[@]} Blender extension(s) in repository '$repository'."