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:
2026-09-10 14:42:17 +08:00
parent 27ca25224a
commit 489003c2cc
136 changed files with 5758 additions and 1083 deletions

View File

@@ -194,18 +194,19 @@ def prepare_model(model):
if model.build:
build_model(model)
return
if model.key == "PISA_SR":
if model.key in {"PISA_SR", "MOGE3_VITL"}:
model.destination.parent.mkdir(parents=True, exist_ok=True)
with TemporaryDirectory(prefix=".pisa-sr-", dir=MODELS_DIR) as directory:
exporter = "pisa_sr_export.py" if model.key == "PISA_SR" else "moge3_export.py"
with TemporaryDirectory(prefix=f".{model.folder}-", dir=MODELS_DIR) as directory:
subprocess.run(
["uv", "run", "--script", str(Path(__file__).with_name("pisa_sr_export.py")),
["uv", "run", "--script", str(Path(__file__).with_name(exporter)),
"--destination", directory],
cwd=PROJECT_ROOT, check=True,
)
models = [item for item in MODEL_FILES if item.key == model.key]
for item in models:
if not verify_file(Path(directory) / item.filename, item.size, item.sha256):
raise RuntimeError("PiSA-SR export differs from the published catalog")
raise RuntimeError(f"{model.key} export differs from the published catalog")
for path in Path(directory).iterdir():
path.replace(model.destination.parent / path.name)
return

View File

@@ -0,0 +1,167 @@
# /// script
# requires-python = ">=3.11,<3.12"
# dependencies = [
# "torch==2.13.0", "torchvision==0.28.0", "onnx==1.22.0", "numpy==2.4.6",
# "triton-windows==3.7.1.post27; sys_platform == 'win32'",
# "moge @ git+https://github.com/microsoft/MoGe.git@74fbce054ebed49800de42d0ad0e83495065719a",
# ]
# [tool.uv.sources]
# torch = { index = "pytorch" }
# torchvision = { index = "pytorch" }
# [[tool.uv.index]]
# name = "pytorch"
# url = "https://download.pytorch.org/whl/cu130"
# explicit = true
# ///
"""Export the dynamic MoGe-3 backbone and sparse refinement ONNX graphs."""
import argparse
import sys
from pathlib import Path
import torch
import torch.nn.functional as F
from moge.model.v3 import MoGeModel
from moge.utils.geometry_torch import normalized_view_plane_uv
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "src" / "anyimage"))
from server.models.onnx_moge3 import build_sparse_inputs
class Moge3Backbone(torch.nn.Module):
def __init__(self, model):
super().__init__()
self.model = model
def forward(self, image, num_tokens):
m = self.model
aspect = image.shape[-1] / image.shape[-2]
bh = (num_tokens / aspect).sqrt().round().long()
bw = (num_tokens * aspect).sqrt().round().long()
feat, cls = m.encoder(image, bh, bw, return_class_token=True)
features = []
for level in range(5):
uv = normalized_view_plane_uv(
width=bw * 2**level,
height=bh * 2**level,
aspect_ratio=aspect,
dtype=image.dtype,
device=image.device,
).permute(2, 0, 1)[None]
features.append(torch.cat([feat, uv], 1) if level == 0 else uv)
neck = m.neck(features)
return (
m.points_head(neck)[-1].permute(0, 2, 3, 1).float(),
features[0],
m.normal_head(neck)[-1],
m.mask_head(neck)[-1],
m.scale_head(cls),
)
class Moge3Refiner(torch.nn.Module):
def __init__(self, r):
super().__init__()
self.r = r
self.n = len(r.down_stages)
def conv(self, m, x, idx):
padded = torch.cat([x, torch.zeros_like(x[:1])], 0)
weights = m.weight.reshape(m.out_channels, 27, m.in_channels).permute(1, 2, 0)
result = padded[idx[:, 0]] @ weights[0]
for k in range(1, 27):
result = result + padded[idx[:, k]] @ weights[k]
return result + m.bias
def stage(self, stage, x, idx):
for b in stage:
y = F.silu(b.norm1(x))
y = F.silu(self.conv(b.conv1, y, idx))
y = self.conv(b.conv2, y, idx)
x = y + b.skip_connection(x)
return x
def forward(self, coord, encoder, *args):
n = self.n
ns = args[:n]
ps = args[n : 2 * n - 1]
us = args[2 * n - 1 : 3 * n - 2]
ei = args[-1]
r = self.r
x = r.input_proj(coord.reshape(-1, 3))
skips = []
for i, stage in enumerate(r.down_stages):
x = self.stage(stage, x, ns[i])
if i < n - 1:
skips.append(x)
padded = torch.cat([x, torch.zeros_like(x[:1])], 0)
count = (ps[i] < x.shape[0]).sum(1, keepdim=True).to(x.dtype)
x = r.downsample_blocks[i].linear(padded[ps[i]].sum(1) / count)
ef = encoder.permute(0, 2, 3, 1).reshape(-1, encoder.shape[1])[ei]
x = r.fuse_proj(torch.cat([x, r.encoder_fuse(ef)], -1))
x = self.stage(r.bottleneck_stage, x, ns[-1])
for i, (up, stage) in enumerate(zip(r.upsample_blocks, r.up_stages)):
level = n - 2 - i
x = up.linear(x)[us[level]] + skips[level]
x = self.stage(stage, x, ns[level])
return r.out_proj(x).reshape(coord.shape[0], coord.shape[1], coord.shape[2])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--destination", type=Path, required=True)
parser.add_argument("--checkpoint", default="Ruicheng/moge-3-vitl")
args = parser.parse_args()
args.destination.mkdir(parents=True, exist_ok=True)
checkpoint_options = (
{"revision": "184008f877d7ad1ad4c2cd2182a9bd1f63d0e5be"}
if args.checkpoint == "Ruicheng/moge-3-vitl"
else {}
)
model = (
MoGeModel.from_pretrained(args.checkpoint, **checkpoint_options).eval().cuda()
)
model.onnx_compatible_mode = True
backbone = Moge3Backbone(model).eval()
image = torch.zeros(1, 3, 192, 256, device="cuda")
with torch.no_grad():
coord, encoder, *_ = backbone(image, torch.tensor(64, device="cuda"))
sparse = build_sparse_inputs(coord.cpu().numpy())
indices = [torch.from_numpy(value).cuda() for value in sparse.values()]
names = ["coord", "encoder", *sparse]
axes = {name: {0: "count_" + name} for name in sparse}
axes.update(
coord={1: "height", 2: "width"},
encoder={2: "token_height", 3: "token_width"},
delta={1: "height", 2: "width"},
)
torch.onnx.export(
Moge3Refiner(model.refiner).eval(),
(coord, encoder, *indices),
str(args.destination / "refiner.onnx"),
input_names=names,
output_names=["delta"],
dynamic_axes=axes,
opset_version=18,
dynamo=False,
)
torch.onnx.export(
backbone,
(image, torch.tensor(1200, device="cuda")),
str(args.destination / "backbone.onnx"),
input_names=["image", "num_tokens"],
output_names=["coord", "encoder", "normal", "mask", "scale"],
dynamic_axes={
"image": {2: "height", 3: "width"},
"coord": {1: "grid_height", 2: "grid_width"},
"encoder": {2: "token_height", 3: "token_width"},
"normal": {2: "grid_height", 3: "grid_width"},
"mask": {2: "grid_height", 3: "grid_width"},
},
opset_version=18,
dynamo=False,
)
if __name__ == "__main__":
main()

View File

@@ -30,6 +30,8 @@
"GeometryNodeFlipFaces": 0,
"GeometryNodeGroup": 1,
"GeometryNodeImageTexture": 2,
"GeometryNodeImageInfo": 0,
"GeometryNodeSetMaterial": 0,
"GeometryNodeInputIndex": 0,
"GeometryNodeInputMeshEdgeNeighbors": 0,
"GeometryNodeInputMeshEdgeVertices": 0,

View File

@@ -13,6 +13,7 @@ from .groups.image_depth_balloon import build_image_depth_balloon_group
from .groups.image_layer import build_image_layer_group
from .groups.image_depth_layer import build_image_depth_layer_group
from .groups.shadeless import build_shadeless_group
from .groups.image_depth_panorama import build_image_depth_panorama_group
OUTPUT_PATH = Path(__file__).resolve().parents[2] / "src/anyimage/assets/O_AnyImage.blend"
@@ -22,6 +23,7 @@ def build_node_groups():
"""Build each asset once in dependency order."""
image_plane = build_image_plane_group()
return [
build_image_depth_panorama_group(),
image_plane,
build_image_depth_plane_group(image_plane),
build_image_relief_plane_group(image_plane),

View File

@@ -7,6 +7,7 @@ import bpy
ASSET_PATH = Path(__file__).resolve().parents[2] / "src/anyimage/assets/O_AnyImage.blend"
GEOMETRY_NAMES = {
"O Image Depth Panorama",
"O Image Plane", "O Image Depth Plane", "O Image Relief Plane",
"O Image Cutout", "O Image Depth Cutout", "O Image Depth Balloon",
}
@@ -17,7 +18,7 @@ def check_asset_library(asset_path):
"""Check library completeness and finite, nonempty evaluated geometry."""
with bpy.data.libraries.load(str(asset_path), link=True) as (source, target):
assert set(source.node_groups) == GEOMETRY_NAMES | SHADER_NAMES
target.node_groups = list(source.node_groups)
target.node_groups = sorted(GEOMETRY_NAMES | SHADER_NAMES)
depth_image = bpy.data.images.new("Asset Check Depth", width=8, height=8, float_buffer=True)
depth_image.colorspace_settings.name = "Non-Color"
depth_image.pixels.foreach_set([

View File

@@ -168,8 +168,8 @@ def _remove_strip_faces(group, geometry, *, triangles=False):
return delete.outputs["Geometry"]
def split_depth_surface(group, geometry, image, strength, uniform, *, triangles=False):
"""Keep each face's camera sample on its own corners before splitting."""
def sample_face_camera(group, image):
"""Sample camera XYZ at each planar face's UV center."""
nodes, links = group.nodes, group.links
uv = nodes.new("GeometryNodeInputNamedAttribute")
uv.data_type = "FLOAT_VECTOR"
@@ -183,9 +183,17 @@ def split_depth_surface(group, geometry, image, strength, uniform, *, triangles=
texture.extension = "EXTEND"
links.new(image, texture.inputs["Image"])
links.new(face_uv.outputs["Value"], texture.inputs["Vector"])
position = nodes.new("GeometryNodeInputPosition")
faces = _store(group, geometry, "face_center", position.outputs[0])
faces = _store(group, faces, "face_camera", texture.outputs["Color"])
return texture.outputs["Color"]
def split_depth_surface(group, geometry, face_camera, strength, uniform, *,
face_center=None, triangles=False):
"""Split adjacent face samples using their Z depth and base center spacing."""
nodes, links = group.nodes, group.links
if face_center is None:
face_center = nodes.new("GeometryNodeInputPosition").outputs[0]
faces = _store(group, geometry, "face_center", face_center)
faces = _store(group, faces, "face_camera", face_camera)
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(_read(group, "face_camera"), axes.inputs[0])
center_field = _read(group, "face_center")

View File

@@ -0,0 +1,22 @@
"""Carry the input mesh's material slots onto generated geometry."""
def inherit_material_slots(group, geometry, source):
nodes, links = group.nodes, group.links
capture = nodes.new("GeometryNodeCaptureAttribute")
capture.domain = "POINT"
capture.capture_items.clear()
capture.capture_items.new("BOOLEAN", "Source")
capture.inputs["Source"].default_value = True
links.new(source, capture.inputs["Geometry"])
join = nodes.new("GeometryNodeJoinGeometry")
links.new(geometry, join.inputs["Geometry"])
links.new(capture.outputs["Geometry"], join.inputs["Geometry"])
material = nodes.new("GeometryNodeSetMaterialIndex")
material.inputs["Material Index"].default_value = 0
links.new(join.outputs["Geometry"], material.inputs["Geometry"])
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain = "POINT"
links.new(material.outputs["Geometry"], delete.inputs["Geometry"])
links.new(capture.outputs["Source"], delete.inputs["Selection"])
return delete.outputs["Geometry"]

View File

@@ -5,7 +5,7 @@ from anyimage.operators.cutout_tool.shape import CUTOUT_Z_TO_X, NORMAL_SCALE_ATT
from ..common.nodes import interface_socket, group_input, math_node, float_input, menu_switch, read_float_attribute, store_float_attribute, prepare_node_group
from ..common.smoothing import expand_smooth
from ..common.cutout import _island_cleanup, _position_field, _remove_zero_thickness, _shade_output
from ..common.depth_surface import project_depth_surface, store_surface_camera, remove_surface_attributes, split_depth_surface, thicken_depth_surface
from ..common.depth_surface import project_depth_surface, store_surface_camera, remove_surface_attributes, split_depth_surface, sample_face_camera, thicken_depth_surface
from ..common.smoothing import edge_boundary_field
@@ -436,7 +436,7 @@ def _build_depth_surface(
nodes = group.nodes
links = group.links
geometry = split_depth_surface(
group, geometry, depth_image, split, uniform_scale, triangles=True
group, geometry, sample_face_camera(group, depth_image), split, uniform_scale, triangles=True
)
original_camera = _position_field(group, depth_image)
geometry, camera_position = store_surface_camera(

View File

@@ -0,0 +1,195 @@
"""Build a quad panorama with periodic sampling and radial depth separation."""
import math
import bpy
from ..common.depth_surface import (
_math, remove_surface_attributes, split_depth_surface, store_surface_camera,
)
from ..common.nodes import float_input, group_input, interface_socket, prepare_node_group
from ..common.material import inherit_material_slots
from ..common.smoothing import expand_smooth
def _quad_sphere(group, segments):
nodes, links = group.nodes, group.links
cube = nodes.new("GeometryNodeMeshCube")
cube.inputs["Size"].default_value = (2, 2, 2)
for axis in "XYZ":
links.new(segments, cube.inputs[f"Vertices {axis}"])
xyz = nodes.new("ShaderNodeSeparateXYZ")
links.new(nodes.new("GeometryNodeInputPosition").outputs[0], xyz.inputs[0])
squares = {axis: _math(group, "MULTIPLY", xyz.outputs[axis], xyz.outputs[axis]) for axis in "XYZ"}
combine = nodes.new("ShaderNodeCombineXYZ")
for axis, a, b in (("X", "Y", "Z"), ("Y", "Z", "X"), ("Z", "X", "Y")):
half = _math(group, "DIVIDE", _math(group, "ADD", squares[a], squares[b]), 2)
third = _math(group, "DIVIDE", _math(group, "MULTIPLY", squares[a], squares[b]), 3)
root = _math(group, "SQRT", _math(group, "ADD", _math(group, "SUBTRACT", 1, half), third))
links.new(_math(group, "MULTIPLY", xyz.outputs[axis], root), combine.inputs[axis])
position = nodes.new("GeometryNodeSetPosition")
links.new(cube.outputs["Mesh"], position.inputs["Geometry"])
links.new(combine.outputs[0], position.inputs["Position"])
return position.outputs["Geometry"]
def build_image_depth_panorama_group():
group = bpy.data.node_groups.new("O Image Depth Panorama", "GeometryNodeTree")
group.is_modifier = group.use_fake_user = True
interface_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
subdivide = interface_socket(group, "Subdivide", "INPUT", "NodeSocketInt")
subdivide.default_value, subdivide.min_value, subdivide.max_value = 7, 0, 8
float_input(group, "Depth Scale", 1, maximum=1, subtype="FACTOR")
float_input(group, "Split Threshold", 0, maximum=1, subtype="FACTOR")
smooth = interface_socket(group, "Smooth", "INPUT", "NodeSocketInt")
smooth.default_value, smooth.min_value, smooth.max_value = 0, 0, 20
interface_socket(group, "Valid Only", "INPUT", "NodeSocketBool").default_value = True
options = group.interface.new_panel("Options", default_closed=True)
float_input(group, "Smooth Weight", 1, maximum=1, subtype="FACTOR", parent=options)
float_input(group, "Validity Threshold", 0.9, maximum=1, subtype="FACTOR", parent=options)
float_input(group, "Invalid Distance", 100, minimum=1e-5, subtype="DISTANCE", parent=options)
data = group.interface.new_panel("Data", default_closed=True)
interface_socket(group, "Depth Image", "INPUT", "NodeSocketImage", data).hide_in_modifier = True
float_input(group, "Reference Distance", 1, minimum=1e-6, subtype="DISTANCE", parent=data).hide_in_modifier = True
interface_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
nodes, links = group.nodes, group.links
def read(name):
node = nodes.new("GeometryNodeInputNamedAttribute")
node.data_type = "FLOAT_VECTOR"
node.inputs["Name"].default_value = name
return node.outputs["Attribute"]
def store(geometry, name, value, domain):
node = nodes.new("GeometryNodeStoreNamedAttribute")
node.data_type, node.domain = ("FLOAT2" if name == "UVMap" else "FLOAT_VECTOR"), domain
node.inputs["Name"].default_value = name
links.new(geometry, node.inputs["Geometry"])
links.new(value, node.inputs["Value"])
return node.outputs["Geometry"]
def uv(direction):
normalize = nodes.new("ShaderNodeVectorMath")
normalize.operation = "NORMALIZE"
links.new(direction, normalize.inputs[0])
xyz = nodes.new("ShaderNodeSeparateXYZ")
links.new(normalize.outputs[0], xyz.inputs[0])
longitude = _math(group, "DIVIDE", _math(group, "ARCTAN2", xyz.outputs["Y"], xyz.outputs["X"]), 2 * math.pi)
z = _math(group, "MAXIMUM", -1, _math(group, "MINIMUM", 1, xyz.outputs["Z"]))
latitude = _math(group, "SUBTRACT", 1, _math(group, "DIVIDE", _math(group, "ARCCOSINE", z), math.pi))
return longitude, latitude, xyz.outputs["Z"]
def combine(u, v):
node = nodes.new("ShaderNodeCombineXYZ")
links.new(u, node.inputs["X"])
links.new(v, node.inputs["Y"])
return node.outputs[0]
def texture(coordinates, interpolation):
inputs = group_input(nodes, {"Depth Image"})
info = nodes.new("GeometryNodeImageInfo")
links.new(inputs.outputs["Depth Image"], info.inputs["Image"])
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(coordinates, axes.inputs[0])
half_pixel = _math(group, "DIVIDE", 0.5, info.outputs["Height"])
v = _math(group, "MAXIMUM", half_pixel,
_math(group, "MINIMUM", axes.outputs["Y"], _math(group, "SUBTRACT", 1, half_pixel)))
node = nodes.new("GeometryNodeImageTexture")
node.interpolation, node.extension = interpolation, "REPEAT"
links.new(inputs.outputs["Depth Image"], node.inputs["Image"])
links.new(combine(axes.outputs["X"], v), node.inputs["Vector"])
return node.outputs["Color"]
def depth(coordinates):
linear, nearest = texture(coordinates, "Linear"), texture(coordinates, "Closest")
alphas = []
for color in (linear, nearest):
separate = nodes.new("FunctionNodeSeparateColor")
links.new(color, separate.inputs["Color"])
alphas.append(separate.outputs["Alpha"])
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "RGBA"
links.new(_math(group, "LESS_THAN", alphas[0],
_math(group, "SUBTRACT", alphas[1], 1e-6)), choose.inputs["Switch"])
links.new(linear, choose.inputs["False"])
links.new(nearest, choose.inputs["True"])
return choose.outputs[0], alphas[1]
inputs = group_input(nodes, {"Subdivide"})
sphere = _quad_sphere(group, _math(group, "ADD", _math(group, "POWER", 2, inputs.outputs["Subdivide"]), 1))
position = nodes.new("GeometryNodeInputPosition").outputs[0]
geometry = store(sphere, "o_panorama_direction", position, "POINT")
longitude, latitude, z = uv(read("o_panorama_direction"))
geometry = store(geometry, "o_panorama_uv", combine(longitude, latitude), "POINT")
face = nodes.new("GeometryNodeFieldOnDomain")
face.data_type, face.domain = "FLOAT_VECTOR", "FACE"
links.new(read("o_panorama_direction"), face.inputs["Value"])
face_u, face_v, _ = uv(face.outputs["Value"])
correction = _math(group, "ROUND", _math(group, "SUBTRACT", face_u, longitude))
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "FLOAT"
links.new(_math(group, "GREATER_THAN", _math(group, "ABSOLUTE", z), 0.999999), choose.inputs["Switch"])
links.new(_math(group, "ADD", longitude, correction), choose.inputs["False"])
links.new(face_u, choose.inputs["True"])
geometry = store(geometry, "UVMap", combine(choose.outputs[0], latitude), "CORNER")
pixel, _ = depth(read("o_panorama_uv"))
geometry = store(geometry, "o_panorama_distance", pixel, "POINT")
nearest = nodes.new("FunctionNodeSeparateColor")
links.new(texture(read("o_panorama_uv"), "Closest"), nearest.inputs["Color"])
controls = group_input(nodes, {"Validity Threshold"})
invalid = _math(group, "MAXIMUM",
_math(group, "LESS_THAN", nearest.outputs["Alpha"], controls.outputs["Validity Threshold"]),
_math(group, "LESS_THAN", nearest.outputs["Red"], 1e-5))
geometry = store(geometry, "o_panorama_invalid", invalid, "POINT")
face_invalid = read("o_panorama_invalid")
any_invalid = _math(group, "GREATER_THAN", face_invalid, 0)
mixed = _math(group, "MULTIPLY", any_invalid,
_math(group, "LESS_THAN", face_invalid, 1))
selection = nodes.new("GeometryNodeSwitch")
selection.input_type = "BOOLEAN"
links.new(group_input(nodes, {"Valid Only"}).outputs["Valid Only"], selection.inputs["Switch"])
links.new(mixed, selection.inputs["False"])
links.new(any_invalid, selection.inputs["True"])
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
links.new(geometry, delete.inputs["Geometry"])
links.new(selection.outputs[0], delete.inputs["Selection"])
controls = group_input(nodes, {"Split Threshold", "Reference Distance"})
strength = controls.outputs["Split Threshold"]
geometry = split_depth_surface(group, delete.outputs[0], depth(combine(face_u, face_v))[0], strength,
_math(group, "DIVIDE", 1, controls.outputs["Reference Distance"]),
face_center=face.outputs["Value"])
geometry, distance = store_surface_camera(group, geometry, read("o_panorama_distance"), strength)
axes = nodes.new("ShaderNodeSeparateXYZ")
links.new(distance, axes.inputs[0])
distance = nodes.new("GeometryNodeSwitch")
distance.input_type = "FLOAT"
links.new(read("o_panorama_invalid"), distance.inputs["Switch"])
links.new(axes.outputs["X"], distance.inputs["False"])
links.new(group_input(nodes, {"Invalid Distance"}).outputs["Invalid Distance"], distance.inputs["True"])
controls = group_input(nodes, {"Depth Scale"})
radius = _math(group, "ADD", 1, _math(group, "MULTIPLY", controls.outputs["Depth Scale"], _math(group, "SUBTRACT", distance.outputs[0], 1)))
radial = nodes.new("ShaderNodeVectorMath")
radial.operation = "SCALE"
links.new(read("o_panorama_direction"), radial.inputs[0])
links.new(radius, radial.inputs[3])
deform = nodes.new("GeometryNodeSetPosition")
links.new(geometry, deform.inputs["Geometry"])
links.new(radial.outputs[0], deform.inputs["Position"])
geometry = remove_surface_attributes(group, deform.outputs[0])
remove = nodes.new("GeometryNodeRemoveAttribute")
remove.pattern_mode = "WILDCARD"
remove.inputs["Name"].default_value = "o_panorama_*"
links.new(geometry, remove.inputs["Geometry"])
geometry = expand_smooth(group, remove.outputs[0], iterations_name="Smooth", weight_name="Smooth Weight")
flip = nodes.new("GeometryNodeFlipFaces")
links.new(geometry, flip.inputs["Mesh"])
smooth = nodes.new("GeometryNodeSetShadeSmooth")
smooth.domain = "FACE"
smooth.inputs["Shade Smooth"].default_value = True
links.new(flip.outputs["Mesh"], smooth.inputs["Geometry"])
geometry = inherit_material_slots(group, smooth.outputs[0], group_input(nodes, {"Geometry"}).outputs["Geometry"])
output = nodes.new("NodeGroupOutput")
links.new(geometry, output.inputs["Geometry"])
prepare_node_group(group)
return group

View File

@@ -7,7 +7,7 @@ from ..common.nodes import prepare_node_group, float_input
from ..common.smoothing import expand_smooth
from ..common.depth_surface import (
project_depth_surface, thicken_depth_surface, remove_surface_attributes,
split_depth_surface, store_surface_camera,
split_depth_surface, sample_face_camera, store_surface_camera,
)
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
@@ -63,7 +63,7 @@ def build_image_depth_plane_group(image_plane_group):
smooth_weight.min_value, smooth_weight.max_value = 0.0, 1.0
smooth_weight.subtype = "FACTOR"
validity_threshold = _interface_socket(group, "Validity Threshold", "INPUT", "NodeSocketFloat", parent=options)
validity_threshold.default_value = 0.9
validity_threshold.default_value = 0.1
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
validity_threshold.subtype = "FACTOR"
data = group.interface.new_panel(name="Data", default_closed=True)
@@ -111,13 +111,10 @@ def build_image_depth_plane_group(image_plane_group):
invalid.data_type, invalid.operation = "FLOAT", "LESS_THAN"
links.new(threshold_input.outputs["Validity Threshold"], invalid.inputs["B"])
links.new(channels.outputs["Alpha"], invalid.inputs["A"])
points = nodes.new("GeometryNodeFieldOnDomain")
points.data_type, points.domain = "BOOLEAN", "POINT"
links.new(invalid.outputs["Result"], points.inputs["Value"])
delete = nodes.new("GeometryNodeDeleteGeometry")
delete.domain, delete.mode = "FACE", "ALL"
links.new(plane.outputs["Geometry"], delete.inputs["Geometry"])
links.new(points.outputs["Value"], delete.inputs["Selection"])
links.new(invalid.outputs["Result"], delete.inputs["Selection"])
cull_controls = _group_input(nodes, {"Valid Only"})
cull = nodes.new("GeometryNodeSwitch")
cull.input_type = "GEOMETRY"
@@ -126,7 +123,7 @@ def build_image_depth_plane_group(image_plane_group):
links.new(plane.outputs["Geometry"], cull.inputs["False"])
split_input = _group_input(nodes, {"Split Threshold"})
geometry = split_depth_surface(
group, cull.outputs["Output"], texture_input.outputs["Depth Image"],
group, cull.outputs["Output"], sample_face_camera(group, texture_input.outputs["Depth Image"]),
split_input.outputs["Split Threshold"], scale_input.outputs["Uniform Scale"],
)
geometry, camera = store_surface_camera(

View File

@@ -3,6 +3,7 @@
import bpy
from ..common.nodes import prepare_node_group
from ..common.material import inherit_material_slots
IMAGE_PLANE_GROUP_NAME = "O Image Plane"
@@ -218,23 +219,8 @@ def build_image_plane_group():
links.new(place_grid.outputs["Geometry"], geometry_switch.inputs["False"])
links.new(place_cube.outputs["Geometry"], geometry_switch.inputs["True"])
capture_source = nodes.new("GeometryNodeCaptureAttribute")
capture_source.domain = "POINT"
capture_source.capture_items.clear()
capture_source.capture_items.new("BOOLEAN", "Source")
capture_source.inputs["Source"].default_value = True
links.new(geometry_input.outputs["Geometry"], capture_source.inputs["Geometry"])
join_material = nodes.new("GeometryNodeJoinGeometry")
links.new(geometry_switch.outputs["Output"], join_material.inputs["Geometry"])
links.new(capture_source.outputs["Geometry"], join_material.inputs["Geometry"])
set_material_index = nodes.new("GeometryNodeSetMaterialIndex")
set_material_index.inputs["Material Index"].default_value = 0
links.new(join_material.outputs["Geometry"], set_material_index.inputs["Geometry"])
delete_source = nodes.new("GeometryNodeDeleteGeometry")
delete_source.domain = "POINT"
links.new(set_material_index.outputs["Geometry"], delete_source.inputs["Geometry"])
links.new(capture_source.outputs["Source"], delete_source.inputs["Selection"])
links.new(delete_source.outputs["Geometry"], output.inputs["Geometry"])
geometry = inherit_material_slots(group, geometry_switch.outputs["Output"], geometry_input.outputs["Geometry"])
links.new(geometry, output.inputs["Geometry"])
group.color_tag = "GEOMETRY"
group.description = (

View File

@@ -14,6 +14,9 @@ def build_shadeless_group():
group.interface.new_socket(
name="Color", in_out="INPUT", socket_type="NodeSocketColor"
)
alpha = group.interface.new_socket(name="Alpha", in_out="INPUT", socket_type="NodeSocketFloat")
alpha.default_value, alpha.min_value, alpha.max_value = 1.0, 0.0, 1.0
alpha.subtype = "FACTOR"
nodes = group.nodes
links = group.links
group_input = nodes.new("NodeGroupInput")
@@ -67,7 +70,15 @@ def build_shadeless_group():
links.new(shader_factor.outputs[0], mix.inputs["Fac"])
links.new(diffuse.outputs["BSDF"], mix.inputs[1])
links.new(emission.outputs["Emission"], mix.inputs[2])
links.new(mix.outputs["Shader"], group_output.inputs["Shader"])
transparent = nodes.new("ShaderNodeBsdfTransparent")
transparent.location = (550.0, -200.0)
alpha_mix = nodes.new("ShaderNodeMixShader")
alpha_mix.location = (850.0, 100.0)
group_output.location = (1100.0, 100.0)
links.new(group_input.outputs["Alpha"], alpha_mix.inputs["Fac"])
links.new(transparent.outputs["BSDF"], alpha_mix.inputs[1])
links.new(mix.outputs["Shader"], alpha_mix.inputs[2])
links.new(alpha_mix.outputs["Shader"], group_output.inputs["Shader"])
group.color_tag = "SHADER"
group.description = (
"Display color without scene lighting while preserving shadows "