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

@@ -71,19 +71,22 @@ def _write_float_exr(rgba, output_path):
return output_path
def write_depth_texture(frame, output_path):
"""Write a frame's camera-space XYZ and validity as float RGBA."""
def write_depth_texture(frame, output_path, *, alpha):
"""Write camera-space XYZ and alpha-weighted model validity as float RGBA."""
import numpy as np
if frame.points is None:
raise ValueError("Depth requires a point field")
points = np.asarray(frame.points, dtype=np.float32)
validity = np.asarray(frame.validity, dtype=np.float32)
alpha = np.asarray(alpha, dtype=np.float32)
if alpha.shape != validity.shape:
raise ValueError("Depth Alpha must match the validity dimensions")
if not np.isfinite(points).all():
raise ValueError("Point field contains non-finite values")
rgba = np.empty((*validity.shape, 4), dtype=np.float32)
rgba[..., :3] = points
rgba[..., 3] = validity
rgba[..., 3] = alpha * validity
return _write_float_exr(rgba, output_path)
@@ -101,7 +104,7 @@ def write_depth_metadata(frame, reference_valid, output_path):
raise ValueError("Depth metadata reference mask dimensions do not match")
if intrinsics.shape != (3, 3):
raise ValueError("Depth metadata requires 3x3 camera intrinsics")
valid = (validity > 0.0) & np.isfinite(depth) & (depth > 0.0)
valid = (validity > 0.5) & np.isfinite(depth) & (depth > 0.0)
reference_valid &= valid
if not valid.any():
raise ValueError("Geometry frame contains no valid depth pixels")

View File

@@ -1,4 +1,4 @@
"""Extend reliable Cutout depth along each visible component's silhouette."""
"""Extend reliable depth along each visible component's silhouette."""
from dataclasses import replace
@@ -18,19 +18,20 @@ def extend_edge_depth(frame, alpha, alpha_threshold=0.9):
return frame
visible = (alpha > 0.0) & (alpha >= alpha_threshold)
opaque = visible & (alpha >= 0.95)
candidates = (
visible
& (alpha >= 0.95)
& (frame.validity > 0.0)
opaque
& (frame.validity > 0.5)
& np.isfinite(depth)
& (depth > 0.0)
)
if not candidates.any():
return frame
interior_distance = ndimage.distance_transform_edt(
np.pad(candidates, 3, mode="edge")
np.pad(opaque, 3, mode="edge")
)[3:-3, 3:-3]
core = candidates & (interior_distance > 2.0)
interior = opaque & (interior_distance > 2.0)
core = candidates & interior
del interior_distance
labels, _ = ndimage.label(visible, structure=np.ones((3, 3), dtype=bool))
corrected = depth.copy()
@@ -59,7 +60,7 @@ def extend_edge_depth(frame, alpha, alpha_threshold=0.9):
& (distance <= 4.0)
& (distance < background_distance[region])
)
target = (component & ~donors) | background
target = (component & ~interior[region] & ~donors) | background
corrected[region][target] = smoothed[target]
background_distance[region][background] = distance[background]

View File

@@ -14,10 +14,8 @@ def _limited_model_input(source_path, output_path, max_input_size):
from PIL import Image
with Image.open(source_path) as opened:
limited = limit_image(opened.convert("RGBA"), max_input_size)
background = Image.new("RGBA", limited.size, (0, 0, 0, 255))
normalized = Image.alpha_composite(background, limited).convert("RGB")
normalized.save(output_path, format="PNG", compress_level=1)
limited = limit_image(opened.convert("RGB"), max_input_size)
limited.save(output_path, format="PNG", compress_level=1)
return output_path
@@ -58,7 +56,6 @@ def generate_moge2_artifacts(
*,
generate_depth=False,
normal_mode="NONE",
correct_edge_depth=False,
alpha_threshold=0.9,
):
"""Run MoGe-2 once and write only the requested file artifacts."""
@@ -85,17 +82,13 @@ def generate_moge2_artifacts(
if generate_depth:
context.progress(0.45, "Generating depth texture")
alpha = _image_alpha(input_path, frame.depth.shape)
depth_frame = (
extend_edge_depth(frame, alpha, alpha_threshold)
if correct_edge_depth else frame
)
depth_frame = extend_edge_depth(frame, alpha, alpha_threshold)
result["depth"] = write_depth_texture(
depth_frame, context.directory / "depth.exr",
depth_frame, context.directory / "depth.exr", alpha=alpha,
)
result["depth_metadata"] = write_depth_metadata(
depth_frame,
(alpha > 0.0) & (alpha >= alpha_threshold)
if correct_edge_depth else alpha >= 0.1,
(alpha > 0.0) & (alpha >= alpha_threshold),
context.directory / "depth.json",
)

View File

@@ -19,7 +19,6 @@ def run(context, parameters):
input_path,
generate_depth=generate_depth,
normal_mode=normal_mode,
correct_edge_depth=generate_depth,
alpha_threshold=float(parameters.get("alpha_threshold", 0.9)),
)
result.update({key: path.name for key, path in artifacts.items()})

View File

@@ -5,6 +5,9 @@ from ..media import copy_color_images, input_frames
def run(context, parameters):
plane_type = parameters["plane_type"]
if plane_type not in {"DEPTH", "RELIEF"}:
raise ValueError(f"Unsupported plane type: {plane_type}")
context.progress(0.05, "Preparing depth conversion")
context.directory.mkdir(parents=True, exist_ok=True)
with input_frames(context, parameters) as images:
@@ -16,7 +19,7 @@ def run(context, parameters):
parameters,
images[0],
generate_depth=True,
normal_mode="TANGENT",
normal_mode="OBJECT" if plane_type == "DEPTH" else "TANGENT",
)
context.check_cancelled()
context.progress(1.0, "Depth conversion data is ready")

View File

@@ -114,11 +114,11 @@ def postprocess(raw_output):
points = np.asarray(raw_output["points"][0], dtype=np.float32)
normal = np.asarray(raw_output["normal"][0], dtype=np.float32)
mask = np.asarray(raw_output["mask"][0] > 0.5)
mask = np.asarray(raw_output["mask"][0], dtype=np.float32)
metric_scale = np.float32(np.asarray(raw_output["metric_scale"]).reshape(-1)[0])
height, width = mask.shape
aspect_ratio = width / height
focal, shift = recover_focal_shift(points, mask)
focal, shift = recover_focal_shift(points, mask > 0.5)
diagonal_factor = (1.0 + aspect_ratio**2) ** 0.5
fx = focal / 2.0 * diagonal_factor / aspect_ratio
fy = focal / 2.0 * diagonal_factor
@@ -127,7 +127,6 @@ def postprocess(raw_output):
dtype=np.float32,
)
depth = points[..., 2] + shift
mask &= depth > 0.0
depth = depth * metric_scale
points = depth_to_points(depth, intrinsics)
return {