feat(node_assets): add image plane geometry nodes and validation scripts
- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness. - Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment. - Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling. - Authored `plane_layouts.py` to define node arrangements for image geometry assets. - Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
This commit is contained in:
@@ -3,7 +3,6 @@ from .cutout_tool import (
|
||||
CutoutSelectionToShape,
|
||||
SelectCutoutSelection,
|
||||
)
|
||||
from .debug import DebugBen2, DebugDa3, DebugMoge2, DebugUpscale
|
||||
from .image_edit_tool import CLASSES as IMAGE_EDIT_TOOL_CLASSES
|
||||
from .convert_to_plane import (
|
||||
ConvertToDepthPlane,
|
||||
@@ -25,10 +24,6 @@ CLASSES = (
|
||||
OpenAIEnvironmentSettings,
|
||||
DownloadModel,
|
||||
DownloadRequiredModels,
|
||||
DebugDa3,
|
||||
DebugMoge2,
|
||||
DebugBen2,
|
||||
DebugUpscale,
|
||||
GenerateDepthPlane,
|
||||
CutoutSelectionToShape,
|
||||
SelectCutoutSelection,
|
||||
|
||||
@@ -147,11 +147,11 @@ def add_image_plane(
|
||||
if width <= 0 or height <= 0:
|
||||
raise PasteTargetError("The clipboard image has invalid dimensions")
|
||||
|
||||
from ..convert_to_plane.mesh_plane import create_mesh_plane_object
|
||||
from ..convert_to_plane.image_plane import create_image_plane_object
|
||||
|
||||
half_width, half_height = _plane_half_dimensions(width, height)
|
||||
material = _create_image_material(image, shadeless, scene=context.scene)
|
||||
plane = create_mesh_plane_object(
|
||||
plane = create_image_plane_object(
|
||||
context,
|
||||
image.name,
|
||||
(-half_width, half_width, -half_height, half_height),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Mesh Plane and Depth Plane conversion operators."""
|
||||
"""Rectangular Plane and Depth Plane conversion operators."""
|
||||
|
||||
from .conversion import (
|
||||
ConvertToDepthPlane,
|
||||
|
||||
@@ -46,7 +46,7 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.generate_depth_plane"
|
||||
bl_label = "Generate Depth Plane"
|
||||
job_type = "generate-depth-plane-geometry"
|
||||
starting_message = "Preparing Depth Plane"
|
||||
starting_message = "Preparing depth conversion"
|
||||
|
||||
input_path: bpy.props.StringProperty(options=_HIDDEN)
|
||||
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
|
||||
@@ -94,7 +94,7 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
|
||||
def start_depth_plane_job(operator, context):
|
||||
source_object = context.object
|
||||
if video_total_frames(source_object.data) != 1:
|
||||
operator.report({"ERROR"}, "Depth Plane currently supports still images")
|
||||
operator.report({"ERROR"}, "Depth conversion currently supports still images")
|
||||
return {"CANCELLED"}
|
||||
setup_result = invoke_ai_setup_if_needed()
|
||||
if setup_result is not None:
|
||||
@@ -157,7 +157,7 @@ class ConvertToPlane(bpy.types.Operator):
|
||||
class ConvertToDepthPlane(bpy.types.Operator):
|
||||
bl_idname = "anyimage.convert_to_depth_plane"
|
||||
bl_label = "Convert to Depth Plane"
|
||||
bl_description = "Convert the complete Image Empty into a front-projected relief"
|
||||
bl_description = "Convert the complete Image Empty into a Relief or Camera depth plane"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
mesh_detail: bpy.props.IntProperty(name="Subdivide", default=6, min=0, max=10)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Create Mesh Plane objects used by conversion and clipboard paste."""
|
||||
"""Create Image Plane objects used by conversion and clipboard paste."""
|
||||
|
||||
import bpy
|
||||
|
||||
@@ -6,10 +6,10 @@ from ...common.object import modifier_input_identifier, set_modifier_input
|
||||
from ...common.node import load_node_group
|
||||
|
||||
|
||||
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
|
||||
IMAGE_PLANE_NODE_GROUP_NAME = "O Image Plane"
|
||||
|
||||
|
||||
def build_mesh_plane_matrix(matrix_world, bounds):
|
||||
def build_image_plane_matrix(matrix_world, bounds):
|
||||
from mathutils import Matrix
|
||||
|
||||
x_min, x_max, y_min, y_max = bounds
|
||||
@@ -17,7 +17,7 @@ def build_mesh_plane_matrix(matrix_world, bounds):
|
||||
return matrix_world @ Matrix.Translation(center)
|
||||
|
||||
|
||||
def create_mesh_plane_mesh(name, bounds):
|
||||
def create_image_plane_mesh(name, bounds):
|
||||
x_min, x_max, y_min, y_max = bounds
|
||||
if x_max <= x_min or y_max <= y_min:
|
||||
raise ValueError("The plane has invalid dimensions")
|
||||
@@ -46,14 +46,14 @@ def create_mesh_plane_mesh(name, bounds):
|
||||
return mesh
|
||||
|
||||
|
||||
def add_mesh_plane_processing(
|
||||
def add_image_plane_processing(
|
||||
plane_object,
|
||||
subdivisions=0,
|
||||
thickness=0.0,
|
||||
):
|
||||
node_group = load_node_group(MESH_PLANE_NODE_GROUP_NAME)
|
||||
node_group = load_node_group(IMAGE_PLANE_NODE_GROUP_NAME)
|
||||
modifier = plane_object.modifiers.new(
|
||||
name=MESH_PLANE_NODE_GROUP_NAME,
|
||||
name=IMAGE_PLANE_NODE_GROUP_NAME,
|
||||
type="NODES",
|
||||
)
|
||||
modifier.node_group = node_group
|
||||
@@ -68,7 +68,7 @@ def add_mesh_plane_processing(
|
||||
return modifier
|
||||
|
||||
|
||||
def create_mesh_plane_object(
|
||||
def create_image_plane_object(
|
||||
context,
|
||||
name,
|
||||
bounds,
|
||||
@@ -77,7 +77,7 @@ def create_mesh_plane_object(
|
||||
subdivisions=0,
|
||||
thickness=0.0,
|
||||
):
|
||||
mesh = create_mesh_plane_mesh(name, bounds)
|
||||
mesh = create_image_plane_mesh(name, bounds)
|
||||
mesh.materials.append(material)
|
||||
plane = bpy.data.objects.new(name, mesh)
|
||||
context.collection.objects.link(plane)
|
||||
@@ -85,8 +85,8 @@ def create_mesh_plane_object(
|
||||
from mathutils import Matrix
|
||||
|
||||
matrix_world = Matrix.Identity(4)
|
||||
plane.matrix_world = build_mesh_plane_matrix(matrix_world, bounds)
|
||||
add_mesh_plane_processing(
|
||||
plane.matrix_world = build_image_plane_matrix(matrix_world, bounds)
|
||||
add_image_plane_processing(
|
||||
plane,
|
||||
subdivisions=subdivisions,
|
||||
thickness=thickness,
|
||||
@@ -23,16 +23,13 @@ from ...common.object import (
|
||||
set_modifier_input,
|
||||
)
|
||||
from ...common.node import load_node_group
|
||||
from .mesh_plane import (
|
||||
build_mesh_plane_matrix,
|
||||
create_mesh_plane_mesh,
|
||||
create_mesh_plane_object,
|
||||
from .image_plane import (
|
||||
build_image_plane_matrix,
|
||||
create_image_plane_mesh,
|
||||
create_image_plane_object,
|
||||
)
|
||||
|
||||
|
||||
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
|
||||
|
||||
|
||||
def create_color_image(source_object):
|
||||
source = source_object.data
|
||||
image = source.copy()
|
||||
@@ -49,7 +46,7 @@ def create_color_image(source_object):
|
||||
|
||||
def create_plane_object(context, source_object, subdivisions, material):
|
||||
source_name = source_object.name
|
||||
plane = create_mesh_plane_object(
|
||||
plane = create_image_plane_object(
|
||||
context,
|
||||
source_name,
|
||||
image_empty_bounds(source_object),
|
||||
@@ -71,35 +68,42 @@ def create_depth_plane_object(
|
||||
depth_image,
|
||||
depth_metadata,
|
||||
):
|
||||
node_group = load_node_group("O Image Depth Plane")
|
||||
source_name = source_object.name
|
||||
bounds = image_empty_bounds(source_object)
|
||||
mesh = create_mesh_plane_mesh(source_name, bounds)
|
||||
for polygon in mesh.polygons:
|
||||
polygon.use_smooth = True
|
||||
mesh.materials.append(material)
|
||||
plane = bpy.data.objects.new(source_name, mesh)
|
||||
context.collection.objects.link(plane)
|
||||
plane.matrix_world = build_mesh_plane_matrix(source_object.matrix_world, bounds)
|
||||
plane["anyimage_mesh_shape"] = "DEPTH_PLANE"
|
||||
mesh = create_image_plane_mesh(source_name, bounds)
|
||||
plane = None
|
||||
try:
|
||||
for polygon in mesh.polygons:
|
||||
polygon.use_smooth = True
|
||||
mesh.materials.append(material)
|
||||
plane = bpy.data.objects.new(source_name, mesh)
|
||||
context.collection.objects.link(plane)
|
||||
plane.matrix_world = build_image_plane_matrix(source_object.matrix_world, bounds)
|
||||
plane["anyimage_mesh_shape"] = "DEPTH_PLANE"
|
||||
|
||||
node_group = load_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
|
||||
modifier = plane.modifiers.new(name=node_group.name, type="NODES")
|
||||
modifier.node_group = node_group
|
||||
modifier.show_group_selector = False
|
||||
uniform_scale = depth_uniform_scale(source_object, depth_metadata)
|
||||
for name, value in (
|
||||
("Subdivide", max(0, min(10, int(subdivisions)))),
|
||||
("Thickness", 0.1),
|
||||
("Depth Scale", 1.0),
|
||||
("Reference Depth", depth_metadata["reference_depth"] * uniform_scale),
|
||||
("Uniform Scale", uniform_scale),
|
||||
("Depth Image", depth_image),
|
||||
):
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(node_group, name),
|
||||
value,
|
||||
)
|
||||
modifier = plane.modifiers.new(name=node_group.name, type="NODES")
|
||||
modifier.node_group = node_group
|
||||
modifier.show_group_selector = False
|
||||
uniform_scale = depth_uniform_scale(source_object, depth_metadata)
|
||||
for name, value in (
|
||||
("Subdivide", max(0, min(10, int(subdivisions)))),
|
||||
("Depth Scale", 1.0),
|
||||
("Reference Depth", depth_metadata["reference_depth"] * uniform_scale),
|
||||
("Uniform Scale", uniform_scale),
|
||||
("Depth Image", depth_image),
|
||||
):
|
||||
set_modifier_input(
|
||||
modifier,
|
||||
modifier_input_identifier(node_group, name),
|
||||
value,
|
||||
)
|
||||
except Exception:
|
||||
if plane is not None:
|
||||
bpy.data.objects.remove(plane, do_unlink=True)
|
||||
if mesh.users == 0:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
raise
|
||||
return finalize_object_result(context, source_object, plane)
|
||||
|
||||
|
||||
@@ -113,7 +117,7 @@ def create_depth_plane_from_result(context, result, operator):
|
||||
try:
|
||||
depth_metadata = load_depth_metadata(result.file("depth_metadata"))
|
||||
depth_image = load_depth_result_image(
|
||||
result.file("z_depth"),
|
||||
result.file("depth"),
|
||||
source_object,
|
||||
depth_metadata,
|
||||
)
|
||||
@@ -141,9 +145,10 @@ def create_depth_plane_from_result(context, result, operator):
|
||||
except Exception:
|
||||
if material is not None and material.users == 0:
|
||||
bpy.data.materials.remove(material, do_unlink=True)
|
||||
for image in (depth_image, normal_image):
|
||||
for image in (color_image, depth_image, normal_image):
|
||||
if image is not None and image.users == 0:
|
||||
bpy.data.images.remove(image, do_unlink=True)
|
||||
raise
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message="Convert to Depth Plane"):
|
||||
raise RuntimeError("Unable to create the Convert to Depth Plane Undo step")
|
||||
label = "Convert to Depth Plane"
|
||||
if "FINISHED" not in bpy.ops.ed.undo_push(message=label):
|
||||
raise RuntimeError(f"Unable to create the {label} Undo step")
|
||||
|
||||
@@ -46,8 +46,6 @@ from ...common.viewport import (
|
||||
ImageGesture,
|
||||
image_edit_drag_keymap,
|
||||
report_image_edit_exception,
|
||||
resolve_image_edit_click,
|
||||
serialize_matrix,
|
||||
)
|
||||
from ...common.selection import (
|
||||
ImageEditWarning,
|
||||
@@ -82,12 +80,8 @@ def normal_mode_for_shape(shape, generate_normal):
|
||||
return NormalMode.TANGENT
|
||||
|
||||
|
||||
def depth_mode_for_shape(shape):
|
||||
if shape == "DEPTH_SOLID":
|
||||
return "VECTOR"
|
||||
if shape == "DEPTH_BALLOON":
|
||||
return "Z"
|
||||
return "NONE"
|
||||
def shape_uses_depth(shape):
|
||||
return shape in {"DEPTH_SOLID", "DEPTH_BALLOON"}
|
||||
|
||||
|
||||
def create_cutout_shape(
|
||||
@@ -322,19 +316,19 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
def request(self, context):
|
||||
require_environment()
|
||||
source_path = require_input_path(self.input_path)
|
||||
depth_mode = depth_mode_for_shape(self.shape)
|
||||
generate_depth = shape_uses_depth(self.shape)
|
||||
normal_mode = normal_mode_for_shape(self.shape, self.generate_normal)
|
||||
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
|
||||
if generate_depth or normal_mode != NormalMode.NONE:
|
||||
require_model(self.model)
|
||||
parameters = {
|
||||
"input": str(source_path),
|
||||
"device": production_device().lower(),
|
||||
"depth_mode": depth_mode,
|
||||
"generate_depth": generate_depth,
|
||||
"normal_mode": normal_mode.value,
|
||||
"max_input_size": int(self.max_ai_input_size),
|
||||
"alpha_threshold": float(self.alpha_threshold),
|
||||
}
|
||||
if depth_mode != "NONE" or normal_mode != NormalMode.NONE:
|
||||
if generate_depth or normal_mode != NormalMode.NONE:
|
||||
start_frame = source_start_frame(self.source_object_name)
|
||||
parameters.update(
|
||||
model=self.model,
|
||||
@@ -388,11 +382,10 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
|
||||
if normal_mode == NormalMode.OBJECT:
|
||||
orient_object_normal_image(normal_image, inward_axis)
|
||||
depth_metadata = None
|
||||
depth_mode = depth_mode_for_shape(self.shape)
|
||||
if depth_mode != "NONE":
|
||||
if shape_uses_depth(self.shape):
|
||||
depth_metadata = load_depth_metadata(result.file("depth_metadata"))
|
||||
depth_image = load_depth_result_image(
|
||||
result.file(f"{depth_mode.lower()}_depth"),
|
||||
result.file("depth"),
|
||||
source_object,
|
||||
depth_metadata,
|
||||
)
|
||||
@@ -437,7 +430,6 @@ class SelectCutoutSelection(ImageGesture, bpy.types.Operator):
|
||||
|
||||
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
|
||||
full_image: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
|
||||
_pie_event = None
|
||||
|
||||
@@ -448,25 +440,7 @@ class SelectCutoutSelection(ImageGesture, bpy.types.Operator):
|
||||
|
||||
def invoke(self, context, event):
|
||||
self._pie_event = event
|
||||
if not self.full_image and event.value != "DOUBLE_CLICK":
|
||||
return super().invoke(context, event)
|
||||
source_object, should_edit = resolve_image_edit_click(
|
||||
context, event, self.report
|
||||
)
|
||||
if not should_edit:
|
||||
return {"FINISHED"} if source_object is not None else {"CANCELLED"}
|
||||
if source_object is None:
|
||||
return {"CANCELLED"}
|
||||
if video_total_frames(source_object.data) > 1:
|
||||
self.report({"WARNING"}, "Cutout only supports a still image")
|
||||
return {"CANCELLED"}
|
||||
self.source_object_name = source_object.name
|
||||
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
|
||||
width, height = (int(value) for value in source_object.data.size)
|
||||
selection_path = SelectionPath(
|
||||
points=((0, 0), (width, 0), (width, height), (0, height)),
|
||||
)
|
||||
return self._submit_selection_path(context, selection_path)
|
||||
return super().invoke(context, event)
|
||||
|
||||
def modal(self, context, event):
|
||||
self._pie_event = event
|
||||
@@ -495,16 +469,9 @@ class CutoutTool(bpy.types.WorkSpaceTool):
|
||||
bl_description = "Lasso an Image Empty and choose a mesh shape"
|
||||
bl_icon = "ops.mesh.primitive_sphere_add_gizmo"
|
||||
bl_operator = SelectCutoutSelection.bl_idname
|
||||
bl_keymap = (
|
||||
(
|
||||
SelectCutoutSelection.bl_idname,
|
||||
{"type": "LEFTMOUSE", "value": "DOUBLE_CLICK"},
|
||||
{"properties": [("full_image", True)]},
|
||||
),
|
||||
*image_edit_drag_keymap(
|
||||
SelectCutoutSelection.bl_idname,
|
||||
deselect_on_empty=False,
|
||||
),
|
||||
bl_keymap = image_edit_drag_keymap(
|
||||
SelectCutoutSelection.bl_idname,
|
||||
deselect_on_empty=False,
|
||||
)
|
||||
|
||||
def draw_settings(context, layout, _tool):
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from ..runtime import JobOperatorBase
|
||||
from ..properties import BEN2_MODEL_KEY, REALESRGAN_GENERAL_WDN_MODEL_KEY
|
||||
from ..common.ai import (
|
||||
require_background_input_path,
|
||||
da3_parameters,
|
||||
require_input_path,
|
||||
moge2_parameters,
|
||||
require_environment,
|
||||
require_model,
|
||||
)
|
||||
|
||||
|
||||
def _open_job_directory(directory):
|
||||
directory = Path(directory)
|
||||
if not directory.is_dir():
|
||||
raise RuntimeError(f"Job directory does not exist: {directory}")
|
||||
result = bpy.ops.wm.path_open(filepath=str(directory))
|
||||
if "FINISHED" not in result:
|
||||
raise RuntimeError(f"Unable to open job directory: {directory}")
|
||||
|
||||
|
||||
class DebugDa3(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.debug_da3"
|
||||
bl_label = "Debug DA3"
|
||||
job_type = "debug-da3"
|
||||
starting_message = "Starting DA3 API inference"
|
||||
|
||||
def request(self, context):
|
||||
require_environment()
|
||||
settings = context.scene.anyimage_settings
|
||||
model = settings.model
|
||||
require_model(model)
|
||||
return {
|
||||
**da3_parameters(
|
||||
settings,
|
||||
require_input_path(settings.input_path),
|
||||
model,
|
||||
settings.process_res,
|
||||
1,
|
||||
),
|
||||
"device": settings.device.lower(),
|
||||
"inference_mode": "joint",
|
||||
"remove_background_model": "",
|
||||
}
|
||||
|
||||
def response(self, _context, result):
|
||||
_open_job_directory(result.directory)
|
||||
return f"Saved DA3 API prediction to {result.directory}"
|
||||
|
||||
|
||||
class DebugMoge2(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.debug_moge2"
|
||||
bl_label = "Debug MoGe-2"
|
||||
job_type = "debug-moge2"
|
||||
starting_message = "Starting MoGe-2 inference"
|
||||
|
||||
def request(self, context):
|
||||
require_environment()
|
||||
settings = context.scene.anyimage_settings
|
||||
require_model(settings.moge_model)
|
||||
return {
|
||||
**moge2_parameters(
|
||||
settings,
|
||||
require_input_path(settings.input_path),
|
||||
settings.moge_model,
|
||||
settings.moge_resolution_level,
|
||||
1,
|
||||
),
|
||||
"device": settings.device.lower(),
|
||||
}
|
||||
|
||||
def response(self, _context, result):
|
||||
_open_job_directory(result.directory)
|
||||
return f"Exported MoGe-2 data to {result.directory}"
|
||||
|
||||
|
||||
class DebugBen2(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.debug_ben2"
|
||||
bl_label = "Debug BEN2"
|
||||
job_type = "debug-ben2"
|
||||
starting_message = "Starting BEN2 background removal"
|
||||
|
||||
def request(self, context):
|
||||
require_environment()
|
||||
settings = context.scene.anyimage_settings
|
||||
require_model(BEN2_MODEL_KEY)
|
||||
return {
|
||||
"input": str(require_background_input_path(settings.background_input_path)),
|
||||
"model": BEN2_MODEL_KEY,
|
||||
"device": settings.device.lower(),
|
||||
"video_frames": settings.video_frames,
|
||||
"start_frame": 1,
|
||||
}
|
||||
|
||||
def response(self, _context, result):
|
||||
_open_job_directory(result.directory)
|
||||
return f"Removed background to {result.directory}"
|
||||
|
||||
|
||||
class DebugUpscale(JobOperatorBase, bpy.types.Operator):
|
||||
bl_idname = "anyimage.debug_upscale"
|
||||
bl_label = "Debug Upscale"
|
||||
job_type = "debug-upscale"
|
||||
starting_message = "Starting Upscale inference"
|
||||
|
||||
model: bpy.props.StringProperty(
|
||||
default=REALESRGAN_GENERAL_WDN_MODEL_KEY,
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def request(self, context):
|
||||
require_environment()
|
||||
settings = context.scene.anyimage_settings
|
||||
require_model(self.model)
|
||||
return {
|
||||
"input": str(require_input_path(settings.input_path)),
|
||||
"model": self.model,
|
||||
"device": settings.device.lower(),
|
||||
"video_frames": settings.video_frames,
|
||||
"start_frame": 1,
|
||||
}
|
||||
|
||||
def response(self, _context, result):
|
||||
_open_job_directory(result.directory)
|
||||
return f"Saved upscale API prediction to {result.directory}"
|
||||
Reference in New Issue
Block a user