saving
This commit is contained in:
parent
ca9ddb196e
commit
ef7fea6252
27
README.md
27
README.md
|
|
@ -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 256px–8K 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 256px–8K 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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'."
|
||||||
|
|
||||||
Loading…
Reference in New Issue