Add new tests and functionality for image processing and geometry handling
- Introduced color_image.py for preparing standalone color images for tests. - Added hdr_image.py to create HDR textures for testing. - Implemented test_image_depth_panorama.py to evaluate panorama topology, UVs, and depth discontinuities. - Created test_shadeless.py to validate the opacity and HDR color endpoints of the Shadeless group. - Developed moge3_export.py for exporting dynamic MoGe-3 backbone and ONNX graphs. - Added material.py to inherit material slots onto generated geometry. - Built image_depth_panorama.py to construct a quad panorama with depth separation and sampling.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
"""Shared Blender-side AI setup, validation, UI, and Job parameters."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from ..server.media.input import validate_input_path
|
||||
|
||||
from ..preferences import addon_preferences
|
||||
from ..properties import (
|
||||
ai_ready,
|
||||
@@ -14,11 +14,6 @@ from ..properties import (
|
||||
)
|
||||
from ..runtime import runtime
|
||||
|
||||
BEN2_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
BEN2_VIDEO_SUFFIXES = {
|
||||
".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v", ".mts", ".mpg", ".mpeg"
|
||||
}
|
||||
|
||||
|
||||
def draw_ai_setup(layout):
|
||||
status = ai_status()
|
||||
@@ -38,21 +33,12 @@ def draw_ai_property(layout, owner, name, status):
|
||||
|
||||
|
||||
def require_input_path(value):
|
||||
path = Path(bpy.path.abspath(value))
|
||||
if path.is_file() or path.is_dir():
|
||||
return path
|
||||
raise ValueError("Select a valid image, video, or frame directory")
|
||||
return validate_input_path(bpy.path.abspath(value))
|
||||
|
||||
|
||||
def require_background_input_path(value):
|
||||
path = require_input_path(value)
|
||||
if path.is_dir() or path.suffix.lower() in (
|
||||
BEN2_IMAGE_SUFFIXES | BEN2_VIDEO_SUFFIXES
|
||||
):
|
||||
return path
|
||||
raise ValueError(
|
||||
"BEN2 requires an image, video, or directory of image frames"
|
||||
)
|
||||
def report_ai_error(operator, error):
|
||||
"""Report the underlying reason from a nested Blender operator."""
|
||||
operator.report({"ERROR"}, str(error).removeprefix("Error: ").strip())
|
||||
|
||||
|
||||
def production_device():
|
||||
@@ -64,10 +50,14 @@ def require_environment():
|
||||
raise RuntimeError("Install the AnyImage Environment first")
|
||||
|
||||
|
||||
def invoke_ai_setup_if_needed():
|
||||
def invoke_ai_setup_if_needed(operator):
|
||||
if ai_ready():
|
||||
return None
|
||||
return bpy.ops.anyimage.setup_ai_environment("INVOKE_DEFAULT")
|
||||
try:
|
||||
return bpy.ops.anyimage.setup_ai_environment("INVOKE_DEFAULT")
|
||||
except RuntimeError as error:
|
||||
report_ai_error(operator, error)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
def require_model(model_key):
|
||||
|
||||
139
src/anyimage/common/color_image.py
Normal file
139
src/anyimage/common/color_image.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Prepare isolated material color files from static image bounds."""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from .image import cleanup_image_input, image_base_name, image_is_packed, image_rgba, is_animated_image, video_total_frames
|
||||
|
||||
|
||||
def require_static_color_image(image):
|
||||
"""Reject animated inputs before preparing material resources."""
|
||||
if is_animated_image(image) or video_total_frames(image) != 1:
|
||||
raise ValueError("Material conversion supports static images only")
|
||||
|
||||
|
||||
def _save_byte_color(rgba, path, color_space):
|
||||
import numpy as np
|
||||
|
||||
height, width = rgba.shape[:2]
|
||||
image = bpy.data.images.new("Material Color Export", width=width, height=height, alpha=True)
|
||||
try:
|
||||
image.colorspace_settings.name = color_space
|
||||
image.alpha_mode = "STRAIGHT"
|
||||
image.pixels.foreach_set(np.flipud(rgba).ravel())
|
||||
image.file_format = "PNG"
|
||||
image.save(filepath=str(path), save_copy=True)
|
||||
if not path.is_file():
|
||||
raise RuntimeError("Unable to export the material color image")
|
||||
finally:
|
||||
bpy.data.images.remove(image)
|
||||
|
||||
|
||||
def prepare_material_color_input(source, bounds=None, rgba=None):
|
||||
"""Export source bounds with interpretation matching the encoded color file."""
|
||||
import numpy as np
|
||||
from .hdr_image import save_hdr_result
|
||||
|
||||
require_static_color_image(source)
|
||||
width, height = map(int, source.size)
|
||||
if (source.is_float and source.source == "FILE" and not source.is_dirty
|
||||
and not image_is_packed(source) and source.alpha_mode == "STRAIGHT"):
|
||||
# Read the encoded float colors without applying a second alpha multiplication.
|
||||
reader = source.copy()
|
||||
try:
|
||||
reader.alpha_mode = "PREMUL"
|
||||
rgba = image_rgba(reader)
|
||||
finally:
|
||||
bpy.data.images.remove(reader)
|
||||
else:
|
||||
rgba = image_rgba(source) if rgba is None else np.asarray(rgba, dtype=np.float32)
|
||||
if rgba.shape != (height, width, 4):
|
||||
raise ValueError("The source RGBA does not match the Blender Image")
|
||||
left, top, right, bottom = (0, 0, width, height) if bounds is None else map(int, bounds)
|
||||
if not (0 <= left < right <= width and 0 <= top < bottom <= height):
|
||||
raise ValueError("The material color bounds are outside the image")
|
||||
cropped = rgba[top:bottom, left:right]
|
||||
suffix = ".exr" if source.is_float else ".png"
|
||||
path = Path(tempfile.mkdtemp(prefix="anyimage-color-")) / ("color" + suffix)
|
||||
try:
|
||||
if source.is_float:
|
||||
# Store float colors directly; the EXR interpretation is applied on load.
|
||||
save_hdr_result(cropped, path)
|
||||
else:
|
||||
_save_byte_color(cropped, path, source.colorspace_settings.name)
|
||||
path.with_suffix(".json").write_text(json.dumps({
|
||||
"name": image_base_name(source) + "_color" + suffix,
|
||||
"color_space": "Linear Rec.709" if source.is_float else source.colorspace_settings.name,
|
||||
"alpha_mode": "PREMUL" if source.is_float and source.alpha_mode == "STRAIGHT" else source.alpha_mode,
|
||||
}), encoding="utf-8")
|
||||
return path
|
||||
except Exception:
|
||||
cleanup_material_color_input(path)
|
||||
raise
|
||||
|
||||
|
||||
def material_analysis_input(color_path):
|
||||
"""Return the byte color file or an HDR recognition preview in the same directory."""
|
||||
from .hdr_image import recognition_rgba
|
||||
|
||||
path = Path(color_path)
|
||||
if path.suffix == ".png":
|
||||
return path
|
||||
image = load_material_color_image(path)
|
||||
try:
|
||||
rgba = image_rgba(image)
|
||||
if image.alpha_mode == "NONE":
|
||||
rgba[..., 3] = 1
|
||||
preview = recognition_rgba(rgba, associated=image.alpha_mode not in {"CHANNEL_PACKED", "NONE"})
|
||||
output = path.parent / "analysis.png"
|
||||
_save_byte_color(preview, output, "sRGB")
|
||||
return output
|
||||
finally:
|
||||
bpy.data.images.remove(image)
|
||||
|
||||
|
||||
def cleanup_material_color_input(path):
|
||||
"""Release the directory owned by a prepared material color input."""
|
||||
if path:
|
||||
cleanup_image_input(Path(path).parent, True)
|
||||
|
||||
|
||||
def load_material_color_image(path):
|
||||
"""Load an independent packed Color image using its file interpretation."""
|
||||
path = Path(path)
|
||||
metadata = json.loads(path.with_suffix(".json").read_text(encoding="utf-8"))
|
||||
image = bpy.data.images.load(str(path), check_existing=False)
|
||||
try:
|
||||
image.name = metadata["name"]
|
||||
image.colorspace_settings.name = metadata["color_space"]
|
||||
image.alpha_mode = metadata["alpha_mode"]
|
||||
image.pack()
|
||||
return image
|
||||
except Exception:
|
||||
bpy.data.images.remove(image)
|
||||
raise
|
||||
|
||||
|
||||
@contextmanager
|
||||
def material_color_image(source=None, *, bounds=None, rgba=None, input_path=None):
|
||||
"""Own a prepared color image through material creation and clean failed results."""
|
||||
owned = input_path is None
|
||||
path = prepare_material_color_input(source, bounds, rgba) if owned else Path(input_path)
|
||||
image = None
|
||||
try:
|
||||
image = load_material_color_image(path)
|
||||
yield image
|
||||
except Exception:
|
||||
if image is not None:
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
image = None
|
||||
raise
|
||||
finally:
|
||||
if image is not None and image.users == 0:
|
||||
bpy.data.images.remove(image)
|
||||
if owned:
|
||||
cleanup_material_color_input(path)
|
||||
177
src/anyimage/common/hdr_image.py
Normal file
177
src/anyimage/common/hdr_image.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""Prepare HDR recognition previews and commit float background-removal results."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
from .image import (
|
||||
cleanup_image_input, image_content_state, image_rgba, is_animated_image,
|
||||
restore_image_content,
|
||||
)
|
||||
from .image_target import has_other_image_user
|
||||
|
||||
|
||||
def recognition_rgba(rgba, *, associated=True):
|
||||
"""Tone-map scene-linear pixels for sRGB recognition without changing the source."""
|
||||
alpha = rgba[..., 3:4]
|
||||
rgb = rgba[..., :3].astype(np.float64)
|
||||
if associated:
|
||||
rgb = np.divide(rgb, alpha, out=np.zeros_like(rgb), where=alpha > 0)
|
||||
rgb = np.maximum(rgb, 0)
|
||||
luminance = rgb @ np.array([0.2126, 0.7152, 0.0722])
|
||||
rgb /= 1 + luminance[..., None]
|
||||
encoded = np.where(rgb <= 0.0031308, rgb * 12.92, 1.055 * rgb ** (1 / 2.4) - 0.055)
|
||||
preview = np.ones(rgba.shape, dtype=np.float32)
|
||||
preview[..., :3] = np.clip(encoded, 0, 1) * alpha
|
||||
return preview
|
||||
|
||||
|
||||
def save_hdr_result(rgba, path):
|
||||
"""Save associated scene-linear RGBA as ZIP-compressed 32-bit EXR."""
|
||||
height, width = rgba.shape[:2]
|
||||
image = bpy.data.images.new("AnyImage HDR Result", width=width, height=height,
|
||||
alpha=True, float_buffer=True)
|
||||
scene = None
|
||||
try:
|
||||
image.alpha_mode = "PREMUL"
|
||||
image.colorspace_settings.name = "Linear Rec.709"
|
||||
image.pixels.foreach_set(np.flipud(rgba).ravel())
|
||||
scene = bpy.data.scenes.new("AnyImage HDR Export")
|
||||
settings = scene.render.image_settings
|
||||
settings.file_format = "OPEN_EXR"
|
||||
settings.color_mode = "RGBA"
|
||||
settings.color_depth = "32"
|
||||
settings.exr_codec = "ZIP"
|
||||
image.save_render(str(path), scene=scene)
|
||||
finally:
|
||||
if scene is not None:
|
||||
bpy.data.scenes.remove(scene)
|
||||
bpy.data.images.remove(image)
|
||||
|
||||
|
||||
def create_packed_hdr(rgba, name, path, *, alpha_mode="PREMUL"):
|
||||
"""Create a self-contained float image and verify its encoded pixels."""
|
||||
result = None
|
||||
save_hdr_result(rgba, path)
|
||||
try:
|
||||
result = bpy.data.images.load(str(path), check_existing=False)
|
||||
result.alpha_mode = alpha_mode
|
||||
if (not result.is_float
|
||||
or result.colorspace_settings.name != "Linear Rec.709"
|
||||
or not np.allclose(image_rgba(result), rgba, rtol=2e-6, atol=1e-7)):
|
||||
raise RuntimeError("The HDR result did not preserve float colors")
|
||||
result.pack()
|
||||
result.name = name
|
||||
return result
|
||||
except Exception:
|
||||
if result is not None:
|
||||
bpy.data.images.remove(result)
|
||||
raise
|
||||
|
||||
|
||||
@dataclass
|
||||
class HdrBackgroundInput:
|
||||
"""Own the submitted pixels and color interpretation of an HDR still."""
|
||||
|
||||
rgba: np.ndarray
|
||||
color_space: str
|
||||
alpha_mode: str
|
||||
source: str
|
||||
path: Path
|
||||
|
||||
@classmethod
|
||||
def prepare(cls, image):
|
||||
if is_animated_image(image):
|
||||
raise RuntimeError("HDR background removal supports still images only")
|
||||
if image.source not in {"FILE", "GENERATED"}:
|
||||
raise RuntimeError("HDR background removal requires a file or generated image")
|
||||
if image.colorspace_settings.is_data:
|
||||
raise RuntimeError("HDR background removal requires a color image with opacity alpha")
|
||||
rgba = image_rgba(image).copy()
|
||||
if not np.isfinite(rgba).all() or np.any((rgba[..., 3] < 0) | (rgba[..., 3] > 1)):
|
||||
raise RuntimeError("HDR image contains invalid color or alpha values")
|
||||
path = Path(tempfile.mkdtemp(prefix="anyimage-hdr-")) / "input.png"
|
||||
preview = None
|
||||
try:
|
||||
height, width = rgba.shape[:2]
|
||||
preview = bpy.data.images.new("AnyImage HDR Preview", width=width, height=height,
|
||||
alpha=True)
|
||||
preview.colorspace_settings.name = "sRGB"
|
||||
preview.alpha_mode = "STRAIGHT"
|
||||
preview_source = rgba.copy()
|
||||
if image.alpha_mode == "NONE":
|
||||
preview_source[..., 3] = 1
|
||||
preview_rgba = recognition_rgba(
|
||||
preview_source, associated=image.alpha_mode not in {"CHANNEL_PACKED", "NONE"},
|
||||
)
|
||||
preview.pixels.foreach_set(np.flipud(preview_rgba).ravel())
|
||||
preview.file_format = "PNG"
|
||||
preview.save(filepath=str(path), save_copy=True)
|
||||
if not path.is_file():
|
||||
raise RuntimeError("Blender did not export the HDR recognition preview")
|
||||
except Exception:
|
||||
cleanup_image_input(path, True)
|
||||
raise
|
||||
finally:
|
||||
if preview is not None:
|
||||
bpy.data.images.remove(preview)
|
||||
return cls(rgba, image.colorspace_settings.name, image.alpha_mode, image.source, path)
|
||||
|
||||
def apply(self, target, alpha_path, *, prepare_undo=False):
|
||||
target.validate()
|
||||
image = target.image
|
||||
if (not image.is_float or image.colorspace_settings.name != self.color_space
|
||||
or image.alpha_mode != self.alpha_mode or image.source != self.source
|
||||
or not np.array_equal(image_rgba(image), self.rgba)):
|
||||
raise RuntimeError("The source HDR image changed; run Remove Background again")
|
||||
try:
|
||||
alpha = np.load(alpha_path, allow_pickle=False)
|
||||
except (OSError, ValueError, EOFError) as error:
|
||||
raise RuntimeError("Unable to read the background alpha result") from error
|
||||
if not isinstance(alpha, np.ndarray):
|
||||
alpha.close()
|
||||
raise RuntimeError("Invalid background alpha result")
|
||||
if (alpha.dtype != np.float32 or alpha.shape != self.rgba.shape[:2]
|
||||
or not np.isfinite(alpha).all() or np.any((alpha < 0) | (alpha > 1))):
|
||||
raise RuntimeError("Invalid background alpha result")
|
||||
# Keep RGB independent of the new mask, including completely transparent pixels.
|
||||
rgba = self.rgba.copy()
|
||||
if self.alpha_mode == "NONE":
|
||||
rgba[..., 3] = 1
|
||||
rgba[..., 3] *= alpha
|
||||
with tempfile.TemporaryDirectory(prefix="anyimage-hdr-result-") as directory:
|
||||
result = create_packed_hdr(
|
||||
rgba, image.name, Path(directory) / "result.exr", alpha_mode="CHANNEL_PACKED",
|
||||
)
|
||||
original_state = None
|
||||
try:
|
||||
if (prepare_undo and (image.is_dirty or image.source == "GENERATED")
|
||||
and not has_other_image_user(target.owner)):
|
||||
original_state = image_content_state(image)
|
||||
baseline_rgba = self.rgba.copy()
|
||||
if self.alpha_mode == "NONE":
|
||||
baseline_rgba[..., 3] = 1
|
||||
baseline = create_packed_hdr(
|
||||
baseline_rgba, image.name, Path(directory) / "source.exr",
|
||||
alpha_mode="CHANNEL_PACKED" if self.alpha_mode == "CHANNEL_PACKED" else "PREMUL",
|
||||
)
|
||||
try:
|
||||
restore_image_content(image, image_content_state(baseline))
|
||||
finally:
|
||||
bpy.data.images.remove(baseline)
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Before HDR Background Removal"):
|
||||
raise RuntimeError("Unable to preserve the source HDR image for Undo")
|
||||
except Exception:
|
||||
bpy.data.images.remove(result)
|
||||
if original_state is not None:
|
||||
restore_image_content(image, original_state)
|
||||
raise
|
||||
try:
|
||||
return target.commit(result, isolate_shared=True)
|
||||
except Exception:
|
||||
if original_state is not None:
|
||||
restore_image_content(image, original_state)
|
||||
raise
|
||||
@@ -8,6 +8,8 @@ from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from ..server.media.input import validate_input_path
|
||||
|
||||
VIEW_TRANSFORM_COLOR_SPACES = (
|
||||
("ACES 1.3", "ACES 1.3 sRGB"),
|
||||
("ACES 2.0", "ACES 2.0 sRGB"),
|
||||
@@ -333,11 +335,6 @@ def extract_frame_range(image, image_user):
|
||||
return start_frame, frame_count
|
||||
|
||||
|
||||
def source_start_frame(source_object_name):
|
||||
source_object = bpy.data.objects.get(source_object_name)
|
||||
return extract_frame_range(source_object.data, source_object.image_user)[0] if source_object else 1
|
||||
|
||||
|
||||
def image_path(image):
|
||||
filepath_from_user = getattr(image, "filepath_from_user", None)
|
||||
filepath = (
|
||||
@@ -395,22 +392,19 @@ def normal_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}_normal.png"
|
||||
|
||||
|
||||
def color_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}_color.png"
|
||||
|
||||
|
||||
def save_packed_image(image, _scene):
|
||||
output_path = Path(tempfile.mkdtemp(prefix="anyimage-input-")) / "input.png"
|
||||
original_file_format = image.file_format
|
||||
try:
|
||||
image.file_format = "PNG"
|
||||
image.save(filepath=str(output_path), save_copy=True)
|
||||
except (OSError, RuntimeError) as error:
|
||||
output_path.unlink(missing_ok=True)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
cleanup_image_input(output_path, True)
|
||||
raise RuntimeError("Unable to export the packed image") from error
|
||||
finally:
|
||||
image.file_format = original_file_format
|
||||
if not output_path.is_file():
|
||||
cleanup_image_input(output_path, True)
|
||||
raise RuntimeError("Blender did not export the packed image")
|
||||
return output_path
|
||||
|
||||
@@ -450,9 +444,11 @@ def prepare_image_input(image, scene):
|
||||
if getattr(image, "source", None) == "MOVIE":
|
||||
if not source_path.is_file():
|
||||
raise RuntimeError(
|
||||
"The image empty must reference an existing video file"
|
||||
f"Source video file does not exist: {source_path}"
|
||||
)
|
||||
return source_path, False
|
||||
if getattr(image, "source", None) == "SEQUENCE":
|
||||
validate_input_path(source_path, images_only=True)
|
||||
sequence_frames = collect_sequence_frames(image)
|
||||
if sequence_frames:
|
||||
temporary_directory = Path(tempfile.mkdtemp(prefix="anyimage-input-"))
|
||||
@@ -469,73 +465,11 @@ def prepare_image_input(image, scene):
|
||||
return temporary_directory, True
|
||||
if not source_path.is_file():
|
||||
raise RuntimeError(
|
||||
"The image empty must reference an existing image file"
|
||||
f"Source image file does not exist: {source_path}"
|
||||
)
|
||||
return source_path, False
|
||||
|
||||
|
||||
def prepare_image_bounds_input(image, source_rgba, bounds):
|
||||
"""Write one rectangular RGBA input owned by its temporary directory."""
|
||||
import numpy as np
|
||||
|
||||
left, top, right, bottom = (int(value) for value in bounds)
|
||||
rgba = np.asarray(source_rgba, dtype=np.float32)
|
||||
expected_shape = (int(image.size[1]), int(image.size[0]), 4)
|
||||
if rgba.shape != expected_shape:
|
||||
raise ValueError("The source RGBA does not match the Blender Image")
|
||||
if (
|
||||
left < 0
|
||||
or top < 0
|
||||
or right > expected_shape[1]
|
||||
or bottom > expected_shape[0]
|
||||
):
|
||||
raise ValueError("The image bounds are outside the Blender Image")
|
||||
if right <= left or bottom <= top:
|
||||
raise ValueError("The image bounds are empty")
|
||||
cropped = rgba[top:bottom, left:right]
|
||||
output_directory = Path(tempfile.mkdtemp(prefix="anyimage-bounds-"))
|
||||
output_path = output_directory / "input.png"
|
||||
temporary_image = bpy.data.images.new(
|
||||
"AnyImage Bounds Input",
|
||||
width=right - left,
|
||||
height=bottom - top,
|
||||
alpha=True,
|
||||
)
|
||||
original_format = temporary_image.file_format
|
||||
try:
|
||||
temporary_image.colorspace_settings.name = image.colorspace_settings.name
|
||||
temporary_image.alpha_mode = "STRAIGHT"
|
||||
temporary_image.pixels.foreach_set(np.flipud(cropped).ravel())
|
||||
temporary_image.file_format = "PNG"
|
||||
temporary_image.save(filepath=str(output_path), save_copy=True)
|
||||
except Exception:
|
||||
cleanup_image_input(output_path, True)
|
||||
raise
|
||||
finally:
|
||||
temporary_image.file_format = original_format
|
||||
bpy.data.images.remove(temporary_image, do_unlink=True)
|
||||
if not output_path.is_file():
|
||||
cleanup_image_input(output_path, True)
|
||||
raise RuntimeError("Blender did not export the bounds image input")
|
||||
return output_path
|
||||
|
||||
|
||||
def load_packed_rgba_image(path, source_image, name):
|
||||
"""Load an encoded local RGBA file and make its Blender data self-contained."""
|
||||
result_image = None
|
||||
try:
|
||||
result_image = bpy.data.images.load(str(path), check_existing=False)
|
||||
result_image.colorspace_settings.name = source_image.colorspace_settings.name
|
||||
result_image.alpha_mode = source_image.alpha_mode
|
||||
result_image.name = name
|
||||
result_image.pack()
|
||||
except (OSError, RuntimeError, TypeError, ValueError) as error:
|
||||
if result_image is not None:
|
||||
bpy.data.images.remove(result_image, do_unlink=True)
|
||||
raise RuntimeError("Unable to prepare the RGBA image result") from error
|
||||
return result_image
|
||||
|
||||
|
||||
def configure_image_user(
|
||||
image_user,
|
||||
frame_count=None,
|
||||
@@ -674,6 +608,15 @@ def has_other_image_empty(source_object, *, removed_objects=()):
|
||||
)
|
||||
|
||||
|
||||
def require_conversion_source(operator):
|
||||
"""Validate the object and image captured when a conversion was submitted."""
|
||||
source = require_image_empty(operator.source_object_name)
|
||||
if (str(source.as_pointer()) != operator.source_identity
|
||||
or str(source.data.as_pointer()) != operator.image_identity):
|
||||
raise ValueError("The conversion source image has changed")
|
||||
return source
|
||||
|
||||
|
||||
def image_content_state(image):
|
||||
"""Capture encoded content and unsaved pixels for an Image transaction."""
|
||||
import numpy as np
|
||||
@@ -728,7 +671,7 @@ def restore_image_content(image, state):
|
||||
|
||||
|
||||
def replace_empty_image(source_object, result_image, *, placement_bounds=None,
|
||||
removed_objects=()):
|
||||
removed_objects=(), isolate_image=False):
|
||||
"""Replace one Empty image, retaining shared source images."""
|
||||
try:
|
||||
source_image = source_object.data
|
||||
@@ -767,7 +710,7 @@ def replace_empty_image(source_object, result_image, *, placement_bounds=None,
|
||||
)
|
||||
original_content = None
|
||||
try:
|
||||
shared = has_other_image_empty(source_object, removed_objects=removed_objects)
|
||||
shared = isolate_image or has_other_image_empty(source_object, removed_objects=removed_objects)
|
||||
if shared:
|
||||
final_image = result_image
|
||||
else:
|
||||
@@ -866,22 +809,6 @@ def load_image_edit_result(
|
||||
)
|
||||
|
||||
|
||||
def load_color_result_image(
|
||||
source_object,
|
||||
output_directory,
|
||||
*,
|
||||
frame_prefix="image.",
|
||||
):
|
||||
"""Load one packed Color texture result for a generated material."""
|
||||
return _load_packed_result_image(
|
||||
source_object.data,
|
||||
output_directory,
|
||||
frame_prefix=frame_prefix,
|
||||
result_name=color_data_name(source_object),
|
||||
alpha_mode=color_alpha_mode(source_object.data.colorspace_settings.name),
|
||||
)
|
||||
|
||||
|
||||
def image_empty_bounds(source_object):
|
||||
image_width, image_height = source_object.data.size
|
||||
if image_width <= 0 or image_height <= 0:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Resolve and commit AI image edits for Empty objects and texture nodes."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from uuid import uuid4
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -56,16 +57,18 @@ def owner_image(owner):
|
||||
return owner.data if is_image_empty(owner) else owner.image
|
||||
|
||||
|
||||
def has_other_texture_image_user(node):
|
||||
def has_other_image_user(owner):
|
||||
"""Check actual Image references, excluding editor display and Fake User."""
|
||||
tree = node.id_data
|
||||
image = node.image
|
||||
tree = None if is_image_empty(owner) else owner.id_data
|
||||
image = owner_image(owner)
|
||||
for user in bpy.data.user_map(subset={image})[image]:
|
||||
if isinstance(user, bpy.types.Screen):
|
||||
continue
|
||||
if user == tree or getattr(user, "node_tree", None) == tree:
|
||||
if user == owner:
|
||||
continue
|
||||
if tree is not None and (user == tree or getattr(user, "node_tree", None) == tree):
|
||||
if any(
|
||||
other != node and getattr(other, "image", None) == image
|
||||
other != owner and getattr(other, "image", None) == image
|
||||
for other in tree.nodes
|
||||
):
|
||||
return True
|
||||
@@ -83,7 +86,7 @@ def replace_texture_image(node, result_image):
|
||||
original_content = None
|
||||
frame_count = video_total_frames(result_image) if is_animated_image(result_image) else None
|
||||
try:
|
||||
if has_other_texture_image_user(node):
|
||||
if has_other_image_user(node):
|
||||
final_image = result_image
|
||||
else:
|
||||
result_content = image_content_state(result_image)
|
||||
@@ -121,6 +124,7 @@ class ImageEditTarget:
|
||||
tree: object = None
|
||||
start_frame: int = 1
|
||||
frame_count: int = 1
|
||||
node_identity: str = ""
|
||||
|
||||
@classmethod
|
||||
def capture(cls, context):
|
||||
@@ -129,10 +133,15 @@ class ImageEditTarget:
|
||||
raise RuntimeError("Select an Image Empty or a material Image Texture")
|
||||
image = owner_image(owner)
|
||||
start_frame, frame_count = extract_frame_range(image, owner.image_user)
|
||||
tree = None if is_image_empty(owner) else owner.id_data
|
||||
identity = ""
|
||||
if tree is not None:
|
||||
identity = owner.get("anyimage_identity")
|
||||
if not identity:
|
||||
identity = uuid4().hex
|
||||
owner["anyimage_identity"] = identity
|
||||
return cls(
|
||||
owner, image,
|
||||
None if is_image_empty(owner) else owner.id_data,
|
||||
start_frame, frame_count,
|
||||
owner, image, tree, start_frame, frame_count, identity,
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
@@ -146,6 +155,7 @@ class ImageEditTarget:
|
||||
and self.image.is_editable
|
||||
and self.tree.nodes.get(self.owner.name) == self.owner
|
||||
and self.owner.image == self.image
|
||||
and self.owner.get("anyimage_identity") == self.node_identity
|
||||
)
|
||||
if valid:
|
||||
return
|
||||
@@ -162,6 +172,19 @@ class ImageEditTarget:
|
||||
def apply(self, directory, *, frame_prefix="image."):
|
||||
self.validate()
|
||||
result_image = load_image_edit_result(self.image, directory, frame_prefix=frame_prefix)
|
||||
return self.commit(result_image)
|
||||
|
||||
def commit(self, result_image, *, isolate_shared=False):
|
||||
"""Commit an owned, fully prepared image through the target transaction."""
|
||||
try:
|
||||
self.validate()
|
||||
except Exception:
|
||||
bpy.data.images.remove(result_image)
|
||||
raise
|
||||
if self.tree is None:
|
||||
if isolate_shared:
|
||||
return replace_empty_image(
|
||||
self.owner, result_image, isolate_image=has_other_image_user(self.owner),
|
||||
)
|
||||
return replace_empty_image(self.owner, result_image)
|
||||
return replace_texture_image(self.owner, result_image)
|
||||
|
||||
@@ -6,10 +6,8 @@ from ..preferences import configured_material_view_adaptation
|
||||
from .image import (
|
||||
VIEW_TRANSFORM_COLOR_SPACES,
|
||||
color_alpha_mode,
|
||||
configure_image_user,
|
||||
image_base_name,
|
||||
image_pixels,
|
||||
is_animated_image,
|
||||
)
|
||||
from .node import load_node_group
|
||||
|
||||
@@ -30,7 +28,9 @@ def material_color_space_name(scene):
|
||||
|
||||
|
||||
def configure_material_color_image(image, scene=None):
|
||||
"""Configure an owned material Color image using the current shading preference."""
|
||||
"""Configure the selected Color image in place using the shading preference."""
|
||||
if getattr(image, "is_float", False):
|
||||
return image
|
||||
if scene is None:
|
||||
scene = getattr(bpy.context, "scene", None)
|
||||
preferred = (
|
||||
@@ -44,11 +44,9 @@ def configure_material_color_image(image, scene=None):
|
||||
or getattr(image, "is_dirty", False)
|
||||
else None
|
||||
)
|
||||
shared = image.users > int(image.use_fake_user)
|
||||
result = image.copy() if shared else image
|
||||
settings = result.colorspace_settings
|
||||
settings = image.colorspace_settings
|
||||
original_space = settings.name
|
||||
original_alpha = result.alpha_mode
|
||||
original_alpha = image.alpha_mode
|
||||
try:
|
||||
for color_space in dict.fromkeys((preferred, "sRGB")):
|
||||
try:
|
||||
@@ -59,21 +57,18 @@ def configure_material_color_image(image, scene=None):
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Unable to configure the material color space")
|
||||
result.alpha_mode = color_alpha_mode(settings.name)
|
||||
image.alpha_mode = color_alpha_mode(settings.name)
|
||||
if pixels is not None:
|
||||
result.pixels.foreach_set(pixels)
|
||||
result.update()
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
except Exception:
|
||||
if shared:
|
||||
bpy.data.images.remove(result)
|
||||
else:
|
||||
settings.name = original_space
|
||||
result.alpha_mode = original_alpha
|
||||
if pixels is not None:
|
||||
result.pixels.foreach_set(pixels)
|
||||
result.update()
|
||||
settings.name = original_space
|
||||
image.alpha_mode = original_alpha
|
||||
if pixels is not None:
|
||||
image.pixels.foreach_set(pixels)
|
||||
image.update()
|
||||
raise
|
||||
return result
|
||||
return image
|
||||
|
||||
|
||||
def material_node_group(depth_plane=False):
|
||||
@@ -93,178 +88,167 @@ def create_image_material(
|
||||
displacement_midlevel=0.0,
|
||||
displacement_scale=1.0,
|
||||
shadeless=False,
|
||||
frame_count=1,
|
||||
frame_start=1,
|
||||
color_frame_offset=0,
|
||||
color_frame_duration=None,
|
||||
depth_frame_offset=0,
|
||||
depth_frame_duration=None,
|
||||
use_cyclic=None,
|
||||
normal_image=None,
|
||||
normal_space="TANGENT",
|
||||
scene=None,
|
||||
texture_extension="EXTEND",
|
||||
):
|
||||
material_image = configure_material_color_image(color_image, scene)
|
||||
preserve_color = color_image.is_float or (
|
||||
shadeless and not color_image.colorspace_settings.name.endswith("sRGB")
|
||||
)
|
||||
material_image = color_image if preserve_color else configure_material_color_image(color_image, scene)
|
||||
if material_image.source == "GENERATED" or material_image.is_dirty:
|
||||
material_image.pack()
|
||||
material = bpy.data.materials.new(image_base_name(source_image))
|
||||
material.use_nodes = True
|
||||
if hasattr(material, "surface_render_method"):
|
||||
material.surface_render_method = "DITHERED"
|
||||
nodes = material.node_tree.nodes
|
||||
nodes.clear()
|
||||
try:
|
||||
material.use_nodes = True
|
||||
if hasattr(material, "surface_render_method"):
|
||||
material.surface_render_method = "DITHERED"
|
||||
nodes = material.node_tree.nodes
|
||||
nodes.clear()
|
||||
|
||||
color_texture = nodes.new("ShaderNodeTexImage")
|
||||
uv_map = nodes.new("ShaderNodeUVMap")
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
color_texture = nodes.new("ShaderNodeTexImage")
|
||||
uv_map = nodes.new("ShaderNodeUVMap")
|
||||
output = nodes.new("ShaderNodeOutputMaterial")
|
||||
|
||||
uv_map.uv_map = "UVMap"
|
||||
uv_map.location = (-1250.0, 350.0)
|
||||
color_texture.image = material_image
|
||||
color_texture.interpolation = "Linear"
|
||||
color_texture.extension = "EXTEND"
|
||||
color_texture.location = (-1000.0, 350.0)
|
||||
output.location = (850.0, 350.0)
|
||||
extracted_frames = frame_count if frame_count > 1 else None
|
||||
color_frames = (
|
||||
color_frame_duration if color_frame_duration is not None else extracted_frames
|
||||
)
|
||||
depth_frames = (
|
||||
depth_frame_duration if depth_frame_duration is not None else extracted_frames
|
||||
)
|
||||
material_frames = color_frames
|
||||
material_frame_offset = color_frame_offset
|
||||
if frame_count > 1 or is_animated_image(material_image):
|
||||
configure_image_user(
|
||||
color_texture.image_user,
|
||||
material_frames,
|
||||
frame_start=frame_start,
|
||||
frame_offset=material_frame_offset,
|
||||
use_cyclic=use_cyclic,
|
||||
)
|
||||
|
||||
links = material.node_tree.links
|
||||
links.new(uv_map.outputs["UV"], color_texture.inputs["Vector"])
|
||||
alpha = color_texture.outputs["Alpha"]
|
||||
material_inputs = nodes.new("ShaderNodeGroup")
|
||||
is_depth_plane = displacement_image is not None
|
||||
material_inputs.node_tree = material_node_group(is_depth_plane)
|
||||
material_inputs.location = (-300.0, 350.0)
|
||||
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
||||
if not is_depth_plane:
|
||||
material_inputs.inputs["Alpha Fix"].default_value = float(
|
||||
configured_material_view_adaptation()
|
||||
)
|
||||
if "Object Space" in material_inputs.inputs:
|
||||
if normal_space not in {"TANGENT", "OBJECT"}:
|
||||
raise ValueError(f"Unsupported normal space: {normal_space}")
|
||||
material_inputs.inputs["Object Space"].default_value = normal_space == "OBJECT"
|
||||
links.new(color_texture.outputs["Color"], material_inputs.inputs["Color"])
|
||||
links.new(alpha, material_inputs.inputs["Alpha"])
|
||||
if normal_image is not None and "Normal" in material_inputs.inputs:
|
||||
normal_texture = nodes.new("ShaderNodeTexImage")
|
||||
normal_texture.image = normal_image
|
||||
normal_texture.interpolation = "Linear"
|
||||
normal_texture.extension = "EXTEND"
|
||||
normal_texture.location = (-1000.0, -50.0)
|
||||
links.new(uv_map.outputs["UV"], normal_texture.inputs["Vector"])
|
||||
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
|
||||
if shadeless:
|
||||
shader = nodes.new("ShaderNodeGroup")
|
||||
shader.node_tree = load_node_group(SHADELESS_NODE_GROUP_NAME)
|
||||
shader.name = SHADELESS_NODE_GROUP_NAME
|
||||
transparent = nodes.new("ShaderNodeBsdfTransparent")
|
||||
mix_shader = nodes.new("ShaderNodeMixShader")
|
||||
shader.location = (0.0, 550.0)
|
||||
transparent.location = (0.0, 150.0)
|
||||
mix_shader.location = (300.0, 350.0)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Alpha"],
|
||||
mix_shader.inputs["Fac"],
|
||||
)
|
||||
links.new(
|
||||
transparent.outputs["BSDF"],
|
||||
mix_shader.inputs[1],
|
||||
)
|
||||
links.new(
|
||||
shader.outputs["Shader"],
|
||||
mix_shader.inputs[2],
|
||||
)
|
||||
links.new(
|
||||
mix_shader.outputs["Shader"],
|
||||
output.inputs["Surface"],
|
||||
)
|
||||
else:
|
||||
shader = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
shader.location = (250.0, 400.0)
|
||||
shader.inputs["IOR"].default_value = 1.2
|
||||
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Base Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Subsurface Radius"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Emission Color"],
|
||||
)
|
||||
links.new(material_inputs.outputs["Alpha"], shader.inputs["Alpha"])
|
||||
links.new(material_inputs.outputs["Normal"], shader.inputs["Normal"])
|
||||
links.new(
|
||||
material_inputs.outputs["Normal"],
|
||||
shader.inputs["Coat Normal"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Roughness"],
|
||||
shader.inputs["Roughness"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Specular IOR Level"],
|
||||
)
|
||||
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
if displacement_image is not None:
|
||||
depth_texture = nodes.new("ShaderNodeTexImage")
|
||||
depth_texture.image = displacement_image
|
||||
depth_texture.interpolation = "Linear"
|
||||
depth_texture.extension = "EXTEND"
|
||||
if frame_count > 1 or is_animated_image(displacement_image):
|
||||
configure_image_user(
|
||||
depth_texture.image_user,
|
||||
depth_frames,
|
||||
frame_start=frame_start,
|
||||
frame_offset=depth_frame_offset,
|
||||
use_cyclic=use_cyclic,
|
||||
uv_map.uv_map = "UVMap"
|
||||
uv_map.location = (-1250.0, 350.0)
|
||||
color_texture.image = material_image
|
||||
color_texture.interpolation = "Linear"
|
||||
color_texture.extension = texture_extension
|
||||
color_texture.location = (-1000.0, 350.0)
|
||||
output.location = (850.0, 350.0)
|
||||
links = material.node_tree.links
|
||||
links.new(uv_map.outputs["UV"], color_texture.inputs["Vector"])
|
||||
if texture_extension == "REPEAT":
|
||||
axes = nodes.new("ShaderNodeSeparateXYZ")
|
||||
axes.location = (-1650.0, 350.0)
|
||||
clamp = nodes.new("ShaderNodeClamp")
|
||||
clamp.location = (-1450.0, 100.0)
|
||||
half_pixel = 0.5 / max(1, int(material_image.size[1]))
|
||||
clamp.inputs["Min"].default_value = half_pixel
|
||||
clamp.inputs["Max"].default_value = 1.0 - half_pixel
|
||||
coordinates = nodes.new("ShaderNodeCombineXYZ")
|
||||
coordinates.location = (-1200.0, 350.0)
|
||||
uv_map.location = (-1900.0, 350.0)
|
||||
links.new(uv_map.outputs["UV"], axes.inputs[0])
|
||||
links.new(axes.outputs["Y"], clamp.inputs["Value"])
|
||||
links.new(axes.outputs["X"], coordinates.inputs["X"])
|
||||
links.new(clamp.outputs[0], coordinates.inputs["Y"])
|
||||
links.new(coordinates.outputs[0], color_texture.inputs["Vector"])
|
||||
alpha = color_texture.outputs["Alpha"]
|
||||
if shadeless and displacement_image is None:
|
||||
shader = nodes.new("ShaderNodeGroup")
|
||||
shader.node_tree = load_node_group(SHADELESS_NODE_GROUP_NAME)
|
||||
shader.location = (0.0, 350.0)
|
||||
links.new(color_texture.outputs["Color"], shader.inputs["Color"])
|
||||
links.new(alpha, shader.inputs["Alpha"])
|
||||
links.new(shader.outputs["Shader"], output.inputs["Surface"])
|
||||
return material
|
||||
material_inputs = nodes.new("ShaderNodeGroup")
|
||||
is_depth_plane = displacement_image is not None
|
||||
material_inputs.node_tree = material_node_group(is_depth_plane)
|
||||
material_inputs.location = (-300.0, 350.0)
|
||||
material_inputs.inputs["Bump Scale"].default_value = depth_scale
|
||||
if not is_depth_plane:
|
||||
material_inputs.inputs["Alpha Fix"].default_value = float(
|
||||
not material_image.is_float and configured_material_view_adaptation()
|
||||
)
|
||||
if "Object Space" in material_inputs.inputs:
|
||||
if normal_space not in {"TANGENT", "OBJECT"}:
|
||||
raise ValueError(f"Unsupported normal space: {normal_space}")
|
||||
material_inputs.inputs["Object Space"].default_value = normal_space == "OBJECT"
|
||||
links.new(color_texture.outputs["Color"], material_inputs.inputs["Color"])
|
||||
links.new(alpha, material_inputs.inputs["Alpha"])
|
||||
if normal_image is not None and "Normal" in material_inputs.inputs:
|
||||
normal_texture = nodes.new("ShaderNodeTexImage")
|
||||
normal_texture.image = normal_image
|
||||
normal_texture.interpolation = "Linear"
|
||||
normal_texture.extension = "EXTEND"
|
||||
normal_texture.location = (-1000.0, -50.0)
|
||||
links.new(uv_map.outputs["UV"], normal_texture.inputs["Vector"])
|
||||
links.new(normal_texture.outputs["Color"], material_inputs.inputs["Normal"])
|
||||
if shadeless:
|
||||
shader = nodes.new("ShaderNodeGroup")
|
||||
shader.node_tree = load_node_group(SHADELESS_NODE_GROUP_NAME)
|
||||
shader.name = SHADELESS_NODE_GROUP_NAME
|
||||
shader.location = (0.0, 550.0)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Alpha"],
|
||||
shader.inputs["Alpha"],
|
||||
)
|
||||
links.new(
|
||||
shader.outputs["Shader"],
|
||||
output.inputs["Surface"],
|
||||
)
|
||||
depth_texture.location = (-1000.0, -400.0)
|
||||
links.new(uv_map.outputs["UV"], depth_texture.inputs["Vector"])
|
||||
displacement_height = depth_texture.outputs["Color"]
|
||||
if displacement_midlevel != 0.0 or displacement_scale != 1.0:
|
||||
depth_adjust = nodes.new("ShaderNodeMath")
|
||||
depth_adjust.operation = "SUBTRACT"
|
||||
depth_adjust.inputs[1].default_value = displacement_midlevel
|
||||
depth_adjust.location = (500.0, -550.0)
|
||||
depth_scale_node = nodes.new("ShaderNodeMath")
|
||||
depth_scale_node.operation = "MULTIPLY"
|
||||
depth_scale_node.inputs[1].default_value = displacement_scale
|
||||
depth_scale_node.location = (700.0, -550.0)
|
||||
links.new(displacement_height, depth_adjust.inputs[0])
|
||||
links.new(depth_adjust.outputs[0], depth_scale_node.inputs[0])
|
||||
links.new(depth_scale_node.outputs[0], material_inputs.inputs["Depth"])
|
||||
else:
|
||||
links.new(displacement_height, material_inputs.inputs["Depth"])
|
||||
links.new(
|
||||
material_inputs.outputs["Displacement"],
|
||||
output.inputs["Displacement"],
|
||||
)
|
||||
set_displacement_only(material)
|
||||
return material
|
||||
shader = nodes.new("ShaderNodeBsdfPrincipled")
|
||||
shader.location = (250.0, 400.0)
|
||||
shader.inputs["IOR"].default_value = 1.2
|
||||
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Base Color"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Subsurface Radius"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Emission Color"],
|
||||
)
|
||||
links.new(material_inputs.outputs["Alpha"], shader.inputs["Alpha"])
|
||||
links.new(material_inputs.outputs["Normal"], shader.inputs["Normal"])
|
||||
links.new(
|
||||
material_inputs.outputs["Normal"],
|
||||
shader.inputs["Coat Normal"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Roughness"],
|
||||
shader.inputs["Roughness"],
|
||||
)
|
||||
links.new(
|
||||
material_inputs.outputs["Color"],
|
||||
shader.inputs["Specular IOR Level"],
|
||||
)
|
||||
links.new(shader.outputs["BSDF"], output.inputs["Surface"])
|
||||
|
||||
if displacement_image is not None:
|
||||
depth_texture = nodes.new("ShaderNodeTexImage")
|
||||
depth_texture.image = displacement_image
|
||||
depth_texture.interpolation = "Linear"
|
||||
depth_texture.extension = "EXTEND"
|
||||
depth_texture.location = (-1000.0, -400.0)
|
||||
links.new(uv_map.outputs["UV"], depth_texture.inputs["Vector"])
|
||||
displacement_height = depth_texture.outputs["Color"]
|
||||
if displacement_midlevel != 0.0 or displacement_scale != 1.0:
|
||||
depth_adjust = nodes.new("ShaderNodeMath")
|
||||
depth_adjust.operation = "SUBTRACT"
|
||||
depth_adjust.inputs[1].default_value = displacement_midlevel
|
||||
depth_adjust.location = (500.0, -550.0)
|
||||
depth_scale_node = nodes.new("ShaderNodeMath")
|
||||
depth_scale_node.operation = "MULTIPLY"
|
||||
depth_scale_node.inputs[1].default_value = displacement_scale
|
||||
depth_scale_node.location = (700.0, -550.0)
|
||||
links.new(displacement_height, depth_adjust.inputs[0])
|
||||
links.new(depth_adjust.outputs[0], depth_scale_node.inputs[0])
|
||||
links.new(depth_scale_node.outputs[0], material_inputs.inputs["Depth"])
|
||||
else:
|
||||
links.new(displacement_height, material_inputs.inputs["Depth"])
|
||||
links.new(
|
||||
material_inputs.outputs["Displacement"],
|
||||
output.inputs["Displacement"],
|
||||
)
|
||||
set_displacement_only(material)
|
||||
return material
|
||||
except Exception:
|
||||
bpy.data.materials.remove(material, do_unlink=True)
|
||||
raise
|
||||
|
||||
|
||||
def set_displacement_only(material):
|
||||
|
||||
Reference in New Issue
Block a user