feat: Add new Geometry Nodes groups for image relief and shadeless shader

- Implemented the "O Image Relief Plane" Geometry Nodes group to create a fixed-base image relief using depth images and various parameters.
- Created the "O Shadeless" shader node group to display color without scene lighting while preserving shadows and reflections.
- Introduced a preview feature for node groups, allowing for interactive visualization of node arrangements in an HTML format.
- Added a template HTML file for rendering node previews with customizable views and controls.
This commit is contained in:
2026-09-09 21:24:57 +08:00
parent f5aff50b13
commit 89087fc2e2
118 changed files with 5635 additions and 3130 deletions

View File

@@ -8,6 +8,7 @@ from .mask_tool import EditImageAlpha
from .rectify_tool import RectifyImagePerspective
from .convert_to_plane import (
ConvertToDepthPlane,
ConvertToReliefPlane,
ConvertToPlane,
GenerateDepthPlane,
)
@@ -38,4 +39,5 @@ CLASSES = (
UpscaleImage,
ConvertToPlane,
ConvertToDepthPlane,
ConvertToReliefPlane,
)

View File

@@ -4,7 +4,7 @@ import tempfile
import bpy
from ...common.material import configure_material_color_image
from ...common.material import create_image_material
CLIPBOARD_HASH_PROPERTY = "anyimage_clipboard_sha256"
CLIPBOARD_TEXTURE_PROPERTY = "anyimage_clipboard_texture"
@@ -150,7 +150,9 @@ def add_image_plane(
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)
material = create_image_material(
image, image, shadeless=shadeless, scene=context.scene
)
plane = create_image_plane_object(
context,
image.name,
@@ -198,52 +200,6 @@ def _plane_half_dimensions(width, height):
return width / height, 1.0
def _create_image_material(image, shadeless=False, *, scene=None):
material_image = configure_material_color_image(image, scene)
material = bpy.data.materials.new(name=f"{image.name} Material")
material.use_nodes = True
if hasattr(material, "surface_render_method"):
material.surface_render_method = "DITHERED"
nodes = material.node_tree.nodes
nodes.clear()
image_node = nodes.new("ShaderNodeTexImage")
image_node.image = material_image
image_node.location = (-620.0, 80.0)
output_node = nodes.new("ShaderNodeOutputMaterial")
output_node.location = (220.0, 80.0)
links = material.node_tree.links
if shadeless:
_create_shadeless_nodes(nodes, links, image_node, output_node)
return material
shader_node = nodes.new("ShaderNodeBsdfPrincipled")
shader_node.location = (-160.0, 80.0)
links.new(image_node.outputs["Color"], shader_node.inputs["Base Color"])
links.new(image_node.outputs["Alpha"], shader_node.inputs["Alpha"])
links.new(shader_node.outputs["BSDF"], output_node.inputs["Surface"])
return material
def _create_shadeless_nodes(nodes, links, image_node, output_node):
transparent_node = nodes.new("ShaderNodeBsdfTransparent")
transparent_node.location = (-360.0, -100.0)
emission_node = nodes.new("ShaderNodeEmission")
emission_node.location = (-360.0, 120.0)
mix_node = nodes.new("ShaderNodeMixShader")
mix_node.location = (-80.0, 80.0)
links.new(image_node.outputs["Color"], emission_node.inputs["Color"])
links.new(image_node.outputs["Alpha"], mix_node.inputs[0])
links.new(transparent_node.outputs["BSDF"], mix_node.inputs[1])
links.new(emission_node.outputs["Emission"], mix_node.inputs[2])
links.new(mix_node.outputs["Shader"], output_node.inputs["Surface"])
def _select_only(context, plane):
for selected_object in getattr(context, "selected_objects", ()):
selected_object.select_set(False)

View File

@@ -2,12 +2,14 @@
from .operators import (
ConvertToDepthPlane,
ConvertToReliefPlane,
ConvertToPlane,
GenerateDepthPlane,
)
__all__ = (
"ConvertToDepthPlane",
"ConvertToReliefPlane",
"ConvertToPlane",
"GenerateDepthPlane",
)

View File

@@ -67,8 +67,12 @@ def create_depth_plane_object(
material,
depth_image,
depth_metadata,
plane_type,
):
node_group = load_node_group("O Image Depth Plane")
node_group = load_node_group({
"DEPTH": "O Image Depth Plane",
"RELIEF": "O Image Relief Plane",
}[plane_type])
source_name = source_object.name
bounds = image_empty_bounds(source_object)
mesh = create_image_plane_mesh(source_name, bounds)
@@ -80,7 +84,7 @@ def create_depth_plane_object(
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"
plane["anyimage_mesh_shape"] = f"{plane_type}_PLANE"
modifier = plane.modifiers.new(name=node_group.name, type="NODES")
modifier.node_group = node_group
@@ -108,6 +112,7 @@ def create_depth_plane_object(
def create_depth_plane_from_result(context, result, operator):
normal_space = {"DEPTH": "OBJECT", "RELIEF": "TANGENT"}[operator.plane_type]
source_object = require_image_empty(operator.source_object_name)
color_image = load_color_result_image(source_object, result.directory)
color_image.name = color_data_name(source_object)
@@ -122,16 +127,14 @@ def create_depth_plane_from_result(context, result, operator):
depth_metadata,
)
normal_image = load_normal_result_image(
result.file("tangent_normal"),
result.file(f"{normal_space.lower()}_normal"),
source_object,
)
material = create_image_material(
source_object,
source_object.data,
color_image,
0.0,
normal_image=normal_image,
normal_space="TANGENT",
ior=1.2,
normal_space=normal_space,
scene=context.scene,
)
create_depth_plane_object(
@@ -141,6 +144,7 @@ def create_depth_plane_from_result(context, result, operator):
material,
depth_image,
depth_metadata,
operator.plane_type,
)
except Exception:
if material is not None and material.users == 0:
@@ -149,6 +153,6 @@ def create_depth_plane_from_result(context, result, operator):
if image is not None and image.users == 0:
bpy.data.images.remove(image, do_unlink=True)
raise
label = "Convert to Depth Plane"
label = f"Convert to {operator.plane_type.title()} Plane"
if "FINISHED" not in bpy.ops.ed.undo_push(message=label):
raise RuntimeError(f"Unable to create the {label} Undo step")

View File

@@ -50,6 +50,11 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
input_path: bpy.props.StringProperty(options=_HIDDEN)
source_object_name: bpy.props.StringProperty(options=_HIDDEN)
plane_type: bpy.props.EnumProperty(
items=(("DEPTH", "Depth Plane", ""), ("RELIEF", "Relief Plane", "")),
default="DEPTH",
options=_HIDDEN,
)
mesh_detail: bpy.props.IntProperty(default=6, min=0, max=10, options=_HIDDEN)
model: bpy.props.StringProperty(default=DEFAULT_MOGE2_MODEL_KEY, options=_HIDDEN)
resolution_level: bpy.props.IntProperty(default=5, min=0, max=9, options=_HIDDEN)
@@ -71,6 +76,7 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
"video_frames": 1,
"start_frame": source_start_frame(self.source_object_name),
"max_input_size": int(self.max_input_size),
"plane_type": self.plane_type,
}
parameters.update(
moge2_parameters(
@@ -85,13 +91,13 @@ class GenerateDepthPlane(JobOperatorBase, bpy.types.Operator):
def response(self, context, result):
create_depth_plane_from_result(context, result, self)
return "Depth Plane is ready"
return f"{self.plane_type.title()} Plane is ready"
def cleanup(self):
cleanup_image_input(self.input_path, self.delete_input)
def start_depth_plane_job(operator, context):
def start_depth_plane_job(operator, context, plane_type):
source_object = context.object
if video_total_frames(source_object.data) != 1:
operator.report({"ERROR"}, "Depth conversion currently supports still images")
@@ -112,6 +118,7 @@ def start_depth_plane_job(operator, context):
input_path=str(source_path),
source_object_name=source_object.name,
mesh_detail=operator.mesh_detail,
plane_type=plane_type,
model=model,
resolution_level=resolution_level,
max_input_size=configured_max_ai_input_size(context),
@@ -141,9 +148,8 @@ class ConvertToPlane(bpy.types.Operator):
source_object
)
material = create_image_material(
source_object,
source_object.data,
color_image,
1.0,
frame_start=frame_start,
color_frame_offset=frame_offset,
color_frame_duration=frame_duration,
@@ -157,7 +163,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 Relief or Camera depth plane"
bl_description = "Convert the complete Image Empty into a camera-projected depth plane"
bl_options = {"REGISTER", "UNDO"}
mesh_detail: bpy.props.IntProperty(name="Subdivide", default=6, min=0, max=10)
@@ -168,8 +174,23 @@ class ConvertToDepthPlane(bpy.types.Operator):
return is_image_empty(context.object) and not runtime.server_busy()
def invoke(self, context, _event):
return self.execute(context)
def execute(self, context):
return start_depth_plane_job(self, context, "DEPTH")
class ConvertToReliefPlane(bpy.types.Operator):
bl_idname = "anyimage.convert_to_relief_plane"
bl_label = "Convert to Relief Plane"
bl_description = "Convert the complete Image Empty into a fixed-base relief plane"
bl_options = {"REGISTER", "UNDO"}
mesh_detail: bpy.props.IntProperty(name="Subdivide", default=6, min=0, max=10)
@classmethod
def poll(cls, context):
from ...runtime import runtime
return is_image_empty(context.object) and not runtime.server_busy()
def execute(self, context):
return start_depth_plane_job(self, context)
return start_depth_plane_job(self, context, "RELIEF")

View File

@@ -192,16 +192,14 @@ def create_shape_object(
mesh_object = None
try:
material = create_image_material(
source_object,
source_object.data,
color_image,
0.0,
normal_image=normal_image,
normal_space=(
NormalMode.TANGENT.value
if normal_mode == NormalMode.NONE
else normal_mode.value
),
ior=1.2,
scene=context.scene,
)
depth_reference = None