Update SnapForm to version 0.10.10 and add technical documentation
- Bump SnapForm version in uv.lock from 0.9.30 to 0.10.10. - Introduce comprehensive technical documentation for SnapForm, detailing features, usage instructions, and development guidelines.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
@@ -22,6 +24,17 @@ IMAGE_NAME_SUFFIXES = (
|
||||
".bmp",
|
||||
".tga",
|
||||
)
|
||||
VIDEO_NAME_SUFFIXES = (
|
||||
".mp4",
|
||||
".mov",
|
||||
".avi",
|
||||
".mkv",
|
||||
".webm",
|
||||
".m4v",
|
||||
".mts",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
)
|
||||
|
||||
|
||||
def image_depth_model_items(owner, context):
|
||||
@@ -47,6 +60,56 @@ def is_image_empty(candidate):
|
||||
)
|
||||
|
||||
|
||||
def is_animated_image(image):
|
||||
return getattr(image, "source", None) in {"MOVIE", "SEQUENCE"}
|
||||
|
||||
|
||||
def video_total_frames(image):
|
||||
"""Return the total animated frame count, or 1 for a still image."""
|
||||
source = getattr(image, "source", None)
|
||||
if source == "MOVIE":
|
||||
return int(getattr(image, "frame_duration", 1) or 1)
|
||||
if source == "SEQUENCE":
|
||||
return len(collect_sequence_frames(image)) or 1
|
||||
return 1
|
||||
|
||||
|
||||
def image_user_settings(source_object):
|
||||
"""Return the Empty's image user settings as a four-item tuple."""
|
||||
image_user = getattr(source_object, "image_user", None)
|
||||
if image_user is None:
|
||||
return 1, 0, 0, False
|
||||
return (
|
||||
int(getattr(image_user, "frame_start", 1) or 1),
|
||||
int(getattr(image_user, "frame_offset", 0) or 0),
|
||||
int(getattr(image_user, "frame_duration", 0) or 0),
|
||||
bool(getattr(image_user, "use_cyclic", False)),
|
||||
)
|
||||
|
||||
|
||||
def extract_frame_range(source_object):
|
||||
"""Return the clamped (start_frame, frame_count) range extracted from the source."""
|
||||
total_frames = video_total_frames(getattr(source_object, "data", None))
|
||||
if total_frames <= 1:
|
||||
return 1, total_frames
|
||||
_frame_start, frame_offset, image_frames, _use_cyclic = (
|
||||
image_user_settings(source_object)
|
||||
)
|
||||
frame_count = (
|
||||
min(image_frames, total_frames)
|
||||
if image_frames > 1
|
||||
else total_frames
|
||||
)
|
||||
start_frame = min(1 + frame_offset, total_frames - frame_count + 1)
|
||||
start_frame = max(start_frame, 1)
|
||||
return start_frame, frame_count
|
||||
|
||||
|
||||
def default_extract_frames(source_object):
|
||||
"""Return the Empty's Image Frames value, or the full frame count when unset."""
|
||||
return extract_frame_range(source_object)[1]
|
||||
|
||||
|
||||
def image_path(image):
|
||||
filepath_from_user = getattr(image, "filepath_from_user", None)
|
||||
filepath = (
|
||||
@@ -65,10 +128,22 @@ def image_is_packed(image):
|
||||
return bool(getattr(image, "packed_files", ()))
|
||||
|
||||
|
||||
def remove_temporary_input(path):
|
||||
"""Remove a temporary input file or frame directory."""
|
||||
path = Path(path)
|
||||
try:
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
else:
|
||||
path.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def image_base_name(image):
|
||||
name = image.name
|
||||
lower_name = name.lower()
|
||||
for suffix in IMAGE_NAME_SUFFIXES:
|
||||
for suffix in IMAGE_NAME_SUFFIXES + VIDEO_NAME_SUFFIXES:
|
||||
suffix_index = lower_name.rfind(suffix)
|
||||
if suffix_index <= 0:
|
||||
continue
|
||||
@@ -85,8 +160,15 @@ def depth_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.depth"
|
||||
|
||||
|
||||
def copy_depth_image(source_object):
|
||||
"""Return a renamed copy of the source image for use as the depth map."""
|
||||
depth_image = source_object.data.copy()
|
||||
depth_image.name = depth_data_name(source_object)
|
||||
return depth_image
|
||||
|
||||
|
||||
def removed_background_data_name(source_object):
|
||||
return f"{image_base_name(source_object.data)}.rb"
|
||||
return f"{image_base_name(source_object.data)}.foreground"
|
||||
|
||||
|
||||
def scaled_process_resolution(image, scale):
|
||||
@@ -117,10 +199,60 @@ def save_packed_image(image, scene):
|
||||
return output_path
|
||||
|
||||
|
||||
def collect_sequence_frames(image):
|
||||
"""Return the numbered frame files of a SEQUENCE image, or [] when absent."""
|
||||
source_path = image_path(image)
|
||||
if not is_animated_image(image) or not source_path.is_file():
|
||||
return []
|
||||
match = re.match(
|
||||
r"^(?P<prefix>.*?)(?P<digits>\d+)(?P<suffix>\.[^.]+)$",
|
||||
source_path.name,
|
||||
)
|
||||
if match is None:
|
||||
return []
|
||||
prefix = match.group("prefix")
|
||||
suffix = match.group("suffix").lower()
|
||||
frames = []
|
||||
for path in source_path.parent.iterdir():
|
||||
frame_match = re.match(
|
||||
r"^(?P<prefix>.*?)(?P<digits>\d+)(?P<suffix>\.[^.]+)$",
|
||||
path.name,
|
||||
)
|
||||
if (
|
||||
frame_match is not None
|
||||
and frame_match.group("prefix") == prefix
|
||||
and frame_match.group("suffix").lower() == suffix
|
||||
):
|
||||
frames.append((int(frame_match.group("digits")), path))
|
||||
return [path for _digits, path in sorted(frames)]
|
||||
|
||||
|
||||
def prepare_image_input(image, scene):
|
||||
source_path = image_path(image)
|
||||
if image_is_packed(image):
|
||||
return save_packed_image(image, scene), True
|
||||
if getattr(image, "source", None) == "MOVIE":
|
||||
if not source_path.is_file():
|
||||
raise RuntimeError(
|
||||
"The image empty must reference an existing video file"
|
||||
)
|
||||
return source_path, False
|
||||
sequence_frames = collect_sequence_frames(image)
|
||||
if sequence_frames:
|
||||
from .runtime import temporary_input_directory
|
||||
|
||||
temporary_directory = Path(temporary_input_directory())
|
||||
try:
|
||||
for index, frame_path in enumerate(sequence_frames):
|
||||
suffix = frame_path.suffix.lower() or ".jpg"
|
||||
shutil.copy2(
|
||||
frame_path,
|
||||
temporary_directory / f"frame.{index + 1:04d}{suffix}",
|
||||
)
|
||||
except Exception:
|
||||
shutil.rmtree(temporary_directory, ignore_errors=True)
|
||||
raise
|
||||
return temporary_directory, True
|
||||
if not source_path.is_file():
|
||||
raise RuntimeError(
|
||||
"The image empty must reference an existing image file"
|
||||
@@ -128,12 +260,207 @@ def prepare_image_input(image, scene):
|
||||
return source_path, False
|
||||
|
||||
|
||||
def configure_image_user(
|
||||
image_user,
|
||||
frame_count=None,
|
||||
frame_start=1,
|
||||
frame_offset=0,
|
||||
use_cyclic=None,
|
||||
):
|
||||
"""Map the image to scene frames and enable auto refresh."""
|
||||
if image_user is None:
|
||||
return
|
||||
if hasattr(image_user, "frame_start"):
|
||||
image_user.frame_start = frame_start
|
||||
if hasattr(image_user, "frame_offset"):
|
||||
image_user.frame_offset = frame_offset
|
||||
if hasattr(image_user, "use_auto_refresh"):
|
||||
image_user.use_auto_refresh = True
|
||||
if use_cyclic is not None and hasattr(image_user, "use_cyclic"):
|
||||
image_user.use_cyclic = use_cyclic
|
||||
if frame_count is not None and hasattr(image_user, "frame_duration"):
|
||||
image_user.frame_duration = frame_count
|
||||
|
||||
|
||||
def pack_sequence_if_possible(image, expected_frames):
|
||||
"""Pack a frame sequence when Blender supports it; otherwise keep external files."""
|
||||
if expected_frames <= 0:
|
||||
return False
|
||||
try:
|
||||
image.pack()
|
||||
except (OSError, RuntimeError):
|
||||
return False
|
||||
if len(getattr(image, "packed_files", ())) >= expected_frames:
|
||||
return True
|
||||
try:
|
||||
image.unpack(method="REMOVE")
|
||||
except (OSError, RuntimeError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def sorted_frame_paths(directory, prefix=None):
|
||||
"""Return image files in a directory sorted by their trailing frame number."""
|
||||
def frame_key(path):
|
||||
match = re.search(r"(\d+)(?=\.[^.]+$)", path.name)
|
||||
return (int(match.group(1)) if match is not None else 0, path.name)
|
||||
|
||||
return sorted(
|
||||
(
|
||||
path
|
||||
for path in Path(directory).iterdir()
|
||||
if path.is_file()
|
||||
and path.suffix.lower() in IMAGE_NAME_SUFFIXES
|
||||
and (prefix is None or path.stem.startswith(prefix))
|
||||
),
|
||||
key=frame_key,
|
||||
)
|
||||
|
||||
|
||||
def force_sequence_source(image):
|
||||
"""Ensure a loaded image is a SEQUENCE datablock for its numbered frames."""
|
||||
if getattr(image, "source", None) == "SEQUENCE":
|
||||
return image
|
||||
try:
|
||||
image.source = "SEQUENCE"
|
||||
except (OSError, RuntimeError, TypeError) as error:
|
||||
raise RuntimeError(
|
||||
"Blender cannot load the frame sequence"
|
||||
) from error
|
||||
if getattr(image, "source", None) != "SEQUENCE":
|
||||
raise RuntimeError("Blender cannot load the frame sequence")
|
||||
return image
|
||||
|
||||
|
||||
def create_sequence_image(name, frame_directory, frame_prefix="frame."):
|
||||
"""Load a numbered frame directory as a SEQUENCE image datablock."""
|
||||
frame_paths = sorted_frame_paths(frame_directory, frame_prefix)
|
||||
if not frame_paths:
|
||||
raise RuntimeError(f"No image frames found in: {frame_directory}")
|
||||
try:
|
||||
image = bpy.data.images.load(
|
||||
str(frame_paths[0]),
|
||||
check_existing=False,
|
||||
)
|
||||
force_sequence_source(image)
|
||||
except (OSError, RuntimeError) as error:
|
||||
if "image" in locals():
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
raise RuntimeError("Unable to load the frame sequence") from error
|
||||
image.name = name
|
||||
pack_sequence_if_possible(image, len(frame_paths))
|
||||
return image
|
||||
|
||||
|
||||
def create_depth_sequence_image(name, depth_frames, scene, frame_directory):
|
||||
"""Write one EXR per depth frame and return a SEQUENCE image datablock."""
|
||||
import numpy as np
|
||||
|
||||
frame_directory = Path(frame_directory)
|
||||
frame_directory.mkdir(parents=True, exist_ok=True)
|
||||
height, width = depth_frames[0].shape
|
||||
image = bpy.data.images.new(
|
||||
name,
|
||||
width=width,
|
||||
height=height,
|
||||
alpha=False,
|
||||
float_buffer=True,
|
||||
)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image_settings = scene.render.image_settings
|
||||
original_file_format = image_settings.file_format
|
||||
frame_paths = []
|
||||
try:
|
||||
image_settings.file_format = "OPEN_EXR"
|
||||
for frame_index, depth_values in enumerate(depth_frames):
|
||||
# NPZ arrays use a top-left origin, while Blender image pixels start below.
|
||||
depth_pixels = np.flipud(depth_values)
|
||||
rgba = np.empty((height, width, 4), dtype=np.float32)
|
||||
rgba[..., 0] = depth_pixels
|
||||
rgba[..., 1] = depth_pixels
|
||||
rgba[..., 2] = depth_pixels
|
||||
rgba[..., 3] = 1.0
|
||||
image.pixels.foreach_set(rgba.ravel())
|
||||
frame_path = (
|
||||
frame_directory / f"depth.{frame_index + 1:04d}.exr"
|
||||
)
|
||||
image.save_render(str(frame_path), scene=scene)
|
||||
if not frame_path.is_file():
|
||||
raise RuntimeError(f"Blender did not write: {frame_path}")
|
||||
frame_paths.append(frame_path)
|
||||
finally:
|
||||
image_settings.file_format = original_file_format
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
if not frame_paths:
|
||||
raise RuntimeError("Unable to write the depth frame sequence")
|
||||
try:
|
||||
sequence_image = bpy.data.images.load(
|
||||
str(frame_paths[0]),
|
||||
check_existing=False,
|
||||
)
|
||||
force_sequence_source(sequence_image)
|
||||
except (OSError, RuntimeError) as error:
|
||||
if "sequence_image" in locals():
|
||||
bpy.data.images.remove(sequence_image, do_unlink=True)
|
||||
raise RuntimeError(
|
||||
"Unable to load the depth frame sequence"
|
||||
) from error
|
||||
sequence_image.name = name
|
||||
sequence_image.colorspace_settings.name = "Non-Color"
|
||||
pack_sequence_if_possible(sequence_image, len(frame_paths))
|
||||
return sequence_image
|
||||
|
||||
|
||||
def create_removed_background_image(
|
||||
source_object,
|
||||
output_path,
|
||||
assign_to_source=True,
|
||||
):
|
||||
output_path = Path(output_path)
|
||||
if output_path.is_dir():
|
||||
foreground_paths = sorted_frame_paths(output_path, "foreground.")
|
||||
if not foreground_paths:
|
||||
raise RuntimeError(
|
||||
f"Background removal result has no foreground frames: "
|
||||
f"{output_path}"
|
||||
)
|
||||
try:
|
||||
result_image = bpy.data.images.load(
|
||||
str(foreground_paths[0]),
|
||||
check_existing=False,
|
||||
)
|
||||
force_sequence_source(result_image)
|
||||
except (OSError, RuntimeError) as error:
|
||||
if "result_image" in locals():
|
||||
bpy.data.images.remove(result_image, do_unlink=True)
|
||||
raise RuntimeError("Unable to load the background result") from error
|
||||
source_image = source_object.data
|
||||
original_fake_user = source_image.use_fake_user
|
||||
try:
|
||||
result_image.name = removed_background_data_name(source_object)
|
||||
pack_sequence_if_possible(result_image, len(foreground_paths))
|
||||
if assign_to_source:
|
||||
image_user = getattr(source_object, "image_user", None)
|
||||
(
|
||||
original_frame_start,
|
||||
_original_frame_offset,
|
||||
_original_frame_duration,
|
||||
original_use_cyclic,
|
||||
) = image_user_settings(source_object)
|
||||
source_image.use_fake_user = True
|
||||
source_object.data = result_image
|
||||
configure_image_user(
|
||||
image_user,
|
||||
len(foreground_paths),
|
||||
frame_start=original_frame_start,
|
||||
use_cyclic=original_use_cyclic,
|
||||
)
|
||||
except (OSError, RuntimeError) as error:
|
||||
source_image.use_fake_user = original_fake_user
|
||||
bpy.data.images.remove(result_image)
|
||||
raise RuntimeError("Unable to load the background result") from error
|
||||
return result_image
|
||||
|
||||
if not output_path.is_file():
|
||||
raise RuntimeError(
|
||||
f"Background removal result does not exist: {output_path}"
|
||||
@@ -229,7 +556,7 @@ def apply_plane_transform(plane_object, source_object, camera_bounds):
|
||||
return abs(float(plane_object.scale.z))
|
||||
|
||||
|
||||
def extract_depth_array(npz_path):
|
||||
def extract_depth_arrays(npz_path):
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError as error:
|
||||
@@ -256,13 +583,17 @@ def extract_depth_array(npz_path):
|
||||
|
||||
if depth.ndim >= 3 and depth.shape[-1] == 1:
|
||||
depth = depth[..., 0]
|
||||
if depth.ndim == 3:
|
||||
depth = depth[0]
|
||||
if depth.ndim != 2:
|
||||
if depth.ndim == 2:
|
||||
depth = depth[None, ...]
|
||||
if depth.ndim != 3:
|
||||
raise ValueError(
|
||||
f"Unsupported depth array shape: {tuple(depth.shape)}"
|
||||
)
|
||||
return depth
|
||||
return [depth[index] for index in range(depth.shape[0])]
|
||||
|
||||
|
||||
def extract_depth_array(npz_path):
|
||||
return extract_depth_arrays(npz_path)[0]
|
||||
|
||||
|
||||
def extract_intrinsics_array(npz_path):
|
||||
@@ -475,6 +806,13 @@ def create_material(
|
||||
displacement_midlevel=0.0,
|
||||
displacement_scale=1.0,
|
||||
unshaded=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,
|
||||
):
|
||||
material = bpy.data.materials.new(name)
|
||||
material.use_nodes = True
|
||||
@@ -491,6 +829,25 @@ def create_material(
|
||||
color_texture.extension = "EXTEND"
|
||||
color_texture.location = (-640.0, 180.0)
|
||||
output.location = (460.0, 180.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
|
||||
)
|
||||
if frame_count > 1 or is_animated_image(color_image):
|
||||
configure_image_user(
|
||||
color_texture.image_user,
|
||||
color_frames,
|
||||
frame_start=frame_start,
|
||||
frame_offset=color_frame_offset,
|
||||
use_cyclic=use_cyclic,
|
||||
)
|
||||
|
||||
links = material.node_tree.links
|
||||
if unshaded:
|
||||
@@ -558,6 +915,14 @@ def create_material(
|
||||
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,
|
||||
)
|
||||
displacement.inputs["Midlevel"].default_value = (
|
||||
displacement_midlevel
|
||||
)
|
||||
@@ -612,8 +977,35 @@ def build_image_depth_plane(
|
||||
displacement_via="MODIFIER",
|
||||
unshaded=False,
|
||||
color_image=None,
|
||||
depth_frames_directory=None,
|
||||
):
|
||||
source_name = source_object.name
|
||||
frame_count = 1
|
||||
frames_image = None
|
||||
if depth_frames_directory is not None:
|
||||
frames_image = create_sequence_image(
|
||||
depth_data_name(source_object),
|
||||
depth_frames_directory,
|
||||
)
|
||||
frame_count = len(
|
||||
sorted_frame_paths(depth_frames_directory, "frame.")
|
||||
)
|
||||
(
|
||||
source_frame_start,
|
||||
source_frame_offset,
|
||||
source_frame_duration,
|
||||
source_use_cyclic,
|
||||
) = image_user_settings(source_object)
|
||||
uses_extracted = frames_image is not None
|
||||
texture_frame_count = (
|
||||
frame_count if uses_extracted else source_frame_duration
|
||||
)
|
||||
texture_frame_offset = 0 if uses_extracted else source_frame_offset
|
||||
depth_source_image = source_object.data
|
||||
if frames_image is not None:
|
||||
depth_source_image = frames_image
|
||||
elif not is_animated_image(source_object.data):
|
||||
depth_source_image = copy_depth_image(source_object)
|
||||
mesh = create_plane_mesh(
|
||||
source_object,
|
||||
subdivision_level=subdivision_level,
|
||||
@@ -632,25 +1024,43 @@ def build_image_depth_plane(
|
||||
depth_data_name(source_object),
|
||||
type="IMAGE",
|
||||
)
|
||||
texture.image = source_object.data
|
||||
texture.image = depth_source_image
|
||||
texture.extension = "EXTEND"
|
||||
if uses_extracted or is_animated_image(source_object.data):
|
||||
configure_image_user(
|
||||
texture.image_user,
|
||||
texture_frame_count,
|
||||
frame_start=source_frame_start,
|
||||
frame_offset=texture_frame_offset,
|
||||
use_cyclic=source_use_cyclic,
|
||||
)
|
||||
displace = plane_object.modifiers.new("Displace", "DISPLACE")
|
||||
displace.texture = texture
|
||||
displace.texture_coords = "UV"
|
||||
displace.mid_level = ORIGINAL_DISPLACEMENT_MIDLEVEL
|
||||
|
||||
depth_scale = abs(float(plane_object.scale.z))
|
||||
displacement_image = (
|
||||
depth_source_image if displacement_via == "MATERIAL" else None
|
||||
)
|
||||
material = create_material(
|
||||
source_name,
|
||||
color_image if color_image is not None else source_object.data,
|
||||
depth_scale,
|
||||
displacement_image=(
|
||||
source_object.data if displacement_via == "MATERIAL" else None
|
||||
),
|
||||
displacement_image=displacement_image,
|
||||
displacement_midlevel=ORIGINAL_DISPLACEMENT_MIDLEVEL,
|
||||
displacement_scale=ORIGINAL_DISPLACEMENT_STRENGTH,
|
||||
unshaded=unshaded,
|
||||
frame_count=frame_count,
|
||||
frame_start=source_frame_start,
|
||||
color_frame_offset=texture_frame_offset,
|
||||
color_frame_duration=texture_frame_count,
|
||||
depth_frame_offset=texture_frame_offset,
|
||||
depth_frame_duration=texture_frame_count,
|
||||
use_cyclic=source_use_cyclic,
|
||||
)
|
||||
mesh.materials.append(material)
|
||||
plane_object["snapform_frame_count"] = frame_count
|
||||
return finish_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
@@ -669,9 +1079,11 @@ def build_depth_plane(
|
||||
unshaded=False,
|
||||
depth_name="",
|
||||
color_image=None,
|
||||
depth_frames_directory=None,
|
||||
):
|
||||
source_name = source_object.name
|
||||
depth = extract_depth_array(npz_path)
|
||||
depth_arrays = extract_depth_arrays(npz_path)
|
||||
depth = depth_arrays[0]
|
||||
intrinsics = extract_intrinsics_array(npz_path)
|
||||
depth_values = prepare_depth_values(depth)
|
||||
bounds, reference_depth = camera_plane_bounds(
|
||||
@@ -681,10 +1093,25 @@ def build_depth_plane(
|
||||
DISPLACEMENT_STRENGTH,
|
||||
)
|
||||
depth_name = depth_name or depth_data_name(source_object)
|
||||
frame_count = len(depth_arrays)
|
||||
(
|
||||
source_frame_start,
|
||||
source_frame_offset,
|
||||
source_frame_duration,
|
||||
source_use_cyclic,
|
||||
) = image_user_settings(source_object)
|
||||
depth_image = create_depth_image(
|
||||
depth_name,
|
||||
depth_values,
|
||||
)
|
||||
if frame_count > 1 and depth_frames_directory is not None:
|
||||
bpy.data.images.remove(depth_image, do_unlink=True)
|
||||
depth_image = create_depth_sequence_image(
|
||||
depth_name,
|
||||
[prepare_depth_values(frame) for frame in depth_arrays],
|
||||
context.scene,
|
||||
depth_frames_directory,
|
||||
)
|
||||
mesh = create_plane_mesh(source_object, bounds, subdivision_level)
|
||||
plane_object = bpy.data.objects.new(
|
||||
source_name,
|
||||
@@ -709,6 +1136,13 @@ def build_depth_plane(
|
||||
)
|
||||
texture.image = depth_image
|
||||
texture.extension = "EXTEND"
|
||||
if frame_count > 1:
|
||||
configure_image_user(
|
||||
texture.image_user,
|
||||
frame_count,
|
||||
frame_start=source_frame_start,
|
||||
use_cyclic=source_use_cyclic,
|
||||
)
|
||||
displace = plane_object.modifiers.new("Displace", "DISPLACE")
|
||||
displace.texture = texture
|
||||
displace.texture_coords = "UV"
|
||||
@@ -726,12 +1160,22 @@ def build_depth_plane(
|
||||
displacement_midlevel=DISPLACEMENT_MIDLEVEL,
|
||||
displacement_scale=DISPLACEMENT_STRENGTH,
|
||||
unshaded=unshaded,
|
||||
frame_count=frame_count,
|
||||
frame_start=source_frame_start,
|
||||
color_frame_offset=0 if color_image is not None else source_frame_offset,
|
||||
color_frame_duration=(
|
||||
frame_count if color_image is not None else source_frame_duration
|
||||
),
|
||||
depth_frame_offset=0,
|
||||
depth_frame_duration=frame_count,
|
||||
use_cyclic=source_use_cyclic,
|
||||
)
|
||||
mesh.materials.append(material)
|
||||
plane_object["snapform_reference_depth"] = reference_depth
|
||||
plane_object["snapform_camera_plane_width"] = bounds[1] - bounds[0]
|
||||
plane_object["snapform_camera_plane_height"] = bounds[3] - bounds[2]
|
||||
plane_object["snapform_depth_scale"] = depth_scale
|
||||
plane_object["snapform_frame_count"] = frame_count
|
||||
|
||||
return finish_depth_plane(
|
||||
context,
|
||||
@@ -758,6 +1202,12 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
snapform_background_path: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_color_frames_dir: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_depth_frames_dir: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_subdivision_level: bpy.props.IntProperty(
|
||||
default=5,
|
||||
min=0,
|
||||
@@ -806,6 +1256,11 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
self.snapform_background_path,
|
||||
assign_to_source=False,
|
||||
)
|
||||
elif self.snapform_color_frames_dir:
|
||||
color_image = create_sequence_image(
|
||||
f"{image_base_name(source_object.data)}.frame",
|
||||
self.snapform_color_frames_dir,
|
||||
)
|
||||
build_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
@@ -816,6 +1271,9 @@ class BuildDepthPlane(bpy.types.Operator):
|
||||
unshaded=self.snapform_unshaded,
|
||||
depth_name=self.snapform_depth_data_name,
|
||||
color_image=color_image,
|
||||
depth_frames_directory=(
|
||||
self.snapform_depth_frames_dir or None
|
||||
),
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
@@ -834,6 +1292,12 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
|
||||
snapform_background_path: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_color_frames_dir: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_depth_frames_dir: bpy.props.StringProperty(
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
snapform_subdivision_level: bpy.props.IntProperty(
|
||||
default=5,
|
||||
min=0,
|
||||
@@ -866,11 +1330,18 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
color_image = create_removed_background_image(
|
||||
source_object,
|
||||
self.snapform_background_path,
|
||||
assign_to_source=False,
|
||||
)
|
||||
color_image = None
|
||||
if self.snapform_background_path:
|
||||
color_image = create_removed_background_image(
|
||||
source_object,
|
||||
self.snapform_background_path,
|
||||
assign_to_source=False,
|
||||
)
|
||||
elif self.snapform_color_frames_dir:
|
||||
color_image = create_sequence_image(
|
||||
f"{image_base_name(source_object.data)}.frame",
|
||||
self.snapform_color_frames_dir,
|
||||
)
|
||||
build_image_depth_plane(
|
||||
context,
|
||||
source_object,
|
||||
@@ -879,6 +1350,9 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
|
||||
displacement_via=self.snapform_displacement_via,
|
||||
unshaded=self.snapform_unshaded,
|
||||
color_image=color_image,
|
||||
depth_frames_directory=(
|
||||
self.snapform_depth_frames_dir or None
|
||||
),
|
||||
)
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
@@ -938,6 +1412,11 @@ class RemoveImageBackground(bpy.types.Operator):
|
||||
from .operators import IMAGE_BACKGROUND_JOB
|
||||
|
||||
source_object = context.object
|
||||
total_frames = video_total_frames(source_object.data)
|
||||
if total_frames > 1:
|
||||
context.scene.snapform_settings.video_frames = (
|
||||
default_extract_frames(source_object)
|
||||
)
|
||||
try:
|
||||
source_path, delete_input = prepare_image_input(
|
||||
source_object.data,
|
||||
@@ -956,7 +1435,7 @@ class RemoveImageBackground(bpy.types.Operator):
|
||||
if "CANCELLED" not in result:
|
||||
return {"FINISHED"}
|
||||
if delete_input:
|
||||
source_path.unlink(missing_ok=True)
|
||||
remove_temporary_input(source_path)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
@@ -1089,13 +1568,45 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
layout.prop(self, "snapform_delete_original")
|
||||
|
||||
def execute(self, context):
|
||||
from .operators import IMAGE_DEPTH_JOB, ORIGINAL_BACKGROUND_JOB
|
||||
from .operators import (
|
||||
IMAGE_DEPTH_JOB,
|
||||
ORIGINAL_BACKGROUND_JOB,
|
||||
ORIGINAL_FRAMES_JOB,
|
||||
)
|
||||
|
||||
source_object = context.object
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
context.scene.snapform_settings.video_frames = (
|
||||
default_extract_frames(source_object)
|
||||
)
|
||||
if (
|
||||
self.snapform_depth_source == "ORIGINAL"
|
||||
and not self.snapform_remove_background
|
||||
):
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
try:
|
||||
source_path, delete_input = prepare_image_input(
|
||||
source_object.data,
|
||||
context.scene,
|
||||
)
|
||||
except RuntimeError as error:
|
||||
self.report({"ERROR"}, str(error))
|
||||
return {"CANCELLED"}
|
||||
result = bpy.ops.snapform.run_job(
|
||||
job_type=ORIGINAL_FRAMES_JOB,
|
||||
snapform_input_path=str(source_path),
|
||||
snapform_source_object_name=source_object.name,
|
||||
snapform_subdivision_level=self.snapform_subdivision_level,
|
||||
snapform_delete_source=self.snapform_delete_original,
|
||||
snapform_delete_input=delete_input,
|
||||
snapform_displacement_via=self.snapform_displacement_via,
|
||||
snapform_unshaded=self.snapform_unshaded,
|
||||
)
|
||||
if "CANCELLED" not in result:
|
||||
return {"FINISHED"}
|
||||
if delete_input:
|
||||
remove_temporary_input(source_path)
|
||||
return {"CANCELLED"}
|
||||
try:
|
||||
build_image_depth_plane(
|
||||
context,
|
||||
@@ -1141,7 +1652,7 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
if "CANCELLED" not in result:
|
||||
return {"FINISHED"}
|
||||
if delete_input:
|
||||
source_path.unlink(missing_ok=True)
|
||||
remove_temporary_input(source_path)
|
||||
return {"CANCELLED"}
|
||||
|
||||
image = source_object.data
|
||||
@@ -1179,7 +1690,7 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
|
||||
)
|
||||
if "CANCELLED" in result:
|
||||
if delete_input:
|
||||
source_path.unlink(missing_ok=True)
|
||||
remove_temporary_input(source_path)
|
||||
return {"CANCELLED"}
|
||||
global _last_depth_plane_model
|
||||
_last_depth_plane_model = self.snapform_model
|
||||
|
||||
Reference in New Issue
Block a user