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:
@@ -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
|
||||
|
||||
167
tools/models/moge3_export.py
Normal file
167
tools/models/moge3_export.py
Normal 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()
|
||||
@@ -30,6 +30,8 @@
|
||||
"GeometryNodeFlipFaces": 0,
|
||||
"GeometryNodeGroup": 1,
|
||||
"GeometryNodeImageTexture": 2,
|
||||
"GeometryNodeImageInfo": 0,
|
||||
"GeometryNodeSetMaterial": 0,
|
||||
"GeometryNodeInputIndex": 0,
|
||||
"GeometryNodeInputMeshEdgeNeighbors": 0,
|
||||
"GeometryNodeInputMeshEdgeVertices": 0,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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")
|
||||
|
||||
22
tools/nodes/common/material.py
Normal file
22
tools/nodes/common/material.py
Normal 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"]
|
||||
@@ -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(
|
||||
|
||||
195
tools/nodes/groups/image_depth_panorama.py
Normal file
195
tools/nodes/groups/image_depth_panorama.py
Normal 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
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user