Files
AnyImage/experiments/panorama_surface/blender_scene.py
nan 27ca25224a feat: Implement depth surface strip cleanup and organize tool CLI
- Added a proposal for depth surface strip cleanup, detailing the rationale and changes to the depth plane and cutout nodes.
- Created specifications for the new cleanup requirements, including scenarios for strip removal and boundary edge detection.
- Developed tasks for implementing shared strip detection logic and integrating new interfaces for depth nodes.
- Established a new CLI structure for organizing node groups and model commands, replacing old entry points with a more intuitive command structure.
- Introduced a prototype for panorama surface generation, including requirements for distance reconstruction and geometry node surfaces.
- Added comprehensive tests for depth strip cleanup functionality, ensuring correct behavior for various mesh topologies and edge cases.
2026-09-10 00:53:43 +08:00

186 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Build or verify a standalone panorama Geometry Nodes demonstration offline."""
import argparse
from pathlib import Path
import sys
import bpy
import numpy as np
from mathutils import Vector
sys.path.insert(0, str(Path(__file__).resolve().parent))
from quad_surface import build_node_group
from validation import evaluated_points, socket_id, verify
def create_distance_image(name, path, output_path):
with np.load(path) as data:
distance, alpha = data["distance"], data["alpha"]
height, width = distance.shape
image = bpy.data.images.new(name, width=width, height=height, alpha=True, float_buffer=True)
image.colorspace_settings.name = "Non-Color"
image.alpha_mode = "CHANNEL_PACKED"
rgba = np.concatenate((np.repeat(distance[..., None], 3, -1), alpha[..., None]), -1)
image.pixels.foreach_set(rgba[::-1].astype(np.float32).ravel())
image.filepath_raw = str(output_path)
image.file_format = "OPEN_EXR"
image.save()
image.pack()
return image
def create_material(image):
material = bpy.data.materials.new("Panorama Color")
material.use_nodes = True
material.node_tree.nodes.clear()
nodes, links = material.node_tree.nodes, material.node_tree.links
uv = nodes.new("ShaderNodeUVMap")
uv.uv_map = "UVMap"
uv.location = (0, 0)
texture = nodes.new("ShaderNodeTexImage")
texture.image = image
texture.extension = "REPEAT"
texture.location = (200, 0)
emission = nodes.new("ShaderNodeEmission")
emission.location = (500, 0)
output = nodes.new("ShaderNodeOutputMaterial")
output.location = (700, 0)
links.new(uv.outputs[0], texture.inputs[0])
links.new(texture.outputs[0], emission.inputs[0])
links.new(emission.outputs[0], output.inputs[0])
return material
def build_scene(output):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = build_node_group()
color = bpy.data.images.load(str(output / "panorama.png"))
color.pack()
material = create_material(color)
objects = []
for name, filename, offset in (("Reference Room", "reference.npz", -4),
("MoGe2 Panorama", "prediction.npz", 4)):
if not (output / filename).exists():
continue
depth_path = output / ("reference_depth.exr" if filename == "reference.npz" else "depth.exr")
distance = create_distance_image(depth_path.name, output / filename, depth_path)
mesh = bpy.data.meshes.new("Panorama Host")
obj = bpy.data.objects.new(name, mesh)
bpy.context.collection.objects.link(obj)
obj.location.x = offset
obj.data.materials.append(material)
modifier = obj.modifiers.new("Panorama Surface", "NODES")
modifier.node_group = group
modifier[socket_id(group, "Distance")] = distance
modifier[socket_id(group, "Subdivide")] = 7
modifier[socket_id(group, "Depth Scale")] = 1.0
modifier[socket_id(group, "Validity Threshold")] = 0.9
modifier[socket_id(group, "Material")] = material
objects.append(obj)
for obj in objects:
obj.select_set(False)
objects[-1].select_set(True)
bpy.context.view_layer.objects.active = objects[-1]
scene = bpy.context.scene
scene.render.engine = "CYCLES"
scene.cycles.samples = 16
scene.render.resolution_x, scene.render.resolution_y = 1400, 750
scene.render.resolution_percentage = 100
scene.world = bpy.data.worlds.new("Trial World")
scene.world.color = (0.18, 0.18, 0.18)
scene.view_settings.view_transform = "Standard"
camera_data = bpy.data.cameras.new("Overview")
camera = bpy.data.objects.new("Overview", camera_data)
bpy.context.collection.objects.link(camera)
camera.location = (11, -17, 13)
camera.rotation_euler = (Vector((0, 0, 0)) - camera.location).to_track_quat("-Z", "Y").to_euler()
camera_data.type, camera_data.ortho_scale = "ORTHO", 20
if len(objects) == 1:
points, _ = evaluated_points(objects[0])
points += np.array(objects[0].location)
center = (points.min(0) + points.max(0)) / 2
extent = float(np.max(np.ptp(points, axis=0)))
camera.location = Vector(center) + Vector((0.6, -1, 1)) * extent
camera.rotation_euler = (Vector(center) - camera.location).to_track_quat("-Z", "Y").to_euler()
camera_data.ortho_scale = extent * 1.8
scene.camera = camera
capture_camera = None
if len(objects) == 1:
capture_data = bpy.data.cameras.new("Capture Center")
capture_data.lens = 22
capture_camera = bpy.data.objects.new("Capture Center", capture_data)
bpy.context.collection.objects.link(capture_camera)
capture_camera.location = objects[0].location
capture_camera.rotation_euler = Vector((-1, 0, 0.15)).to_track_quat("-Z", "Y").to_euler()
bpy.data.texts.new("Attribution").write(
(Path(__file__).parent / "ATTRIBUTION.md").read_text(encoding="utf-8"))
bpy.data.texts.new("试验说明").write(
"Subdivide 控制 Quad Sphere 细分,全有效面数为 6 × 4^Subdivide。\n"
"Depth Scale:0 为单位球面,1 为全景距离重建。\n"
"深度 EXR 的 Alpha 已包含 Mask × 源图 Alpha,低于 Validity Threshold 的区域会被剔除。\n"
"Quad Sphere 来自 O_Essentials,节点组和依赖已打包。\n"
)
for screen in bpy.data.screens:
for area in screen.areas:
if area.type == "VIEW_3D":
area.spaces.active.shading.type = "MATERIAL"
area.spaces.active.region_3d.view_distance = extent * 1.4 if len(objects) == 1 else 18
if len(objects) == 1:
area.spaces.active.region_3d.view_location = Vector(center)
area.spaces.active.region_3d.view_rotation = camera.rotation_euler.to_quaternion()
verify(output)
bpy.ops.wm.save_as_mainfile(filepath=str(output / "panorama_trial.blend"))
# The render uses a temporary inspection cut; the saved asset retains the full shell.
for obj in objects:
if len(objects) == 1:
continue
modifier = obj.modifiers.new("Inspection Cut", "NODES")
cut_group = bpy.data.node_groups.new(f"{obj.name} Inspection", "GeometryNodeTree")
cut_group.interface.new_socket(name="Geometry", in_out="INPUT", socket_type="NodeSocketGeometry")
cut_group.interface.new_socket(name="Geometry", in_out="OUTPUT", socket_type="NodeSocketGeometry")
source = cut_group.nodes.new("NodeGroupInput")
position = cut_group.nodes.new("GeometryNodeInputPosition")
xyz = cut_group.nodes.new("ShaderNodeSeparateXYZ")
above = cut_group.nodes.new("ShaderNodeMath")
above.operation = "GREATER_THAN"
above.inputs[1].default_value = 0.4
front = cut_group.nodes.new("ShaderNodeMath")
front.operation = "LESS_THAN"
front.inputs[1].default_value = -0.7
either = cut_group.nodes.new("ShaderNodeMath")
either.operation = "MAXIMUM"
remove = cut_group.nodes.new("GeometryNodeDeleteGeometry")
remove.domain = "FACE"
result = cut_group.nodes.new("NodeGroupOutput")
for a, b in ((position.outputs[0], xyz.inputs[0]),
(xyz.outputs["Z"], above.inputs[0]), (xyz.outputs["Y"], front.inputs[0]),
(above.outputs[0], either.inputs[0]), (front.outputs[0], either.inputs[1]),
(source.outputs[0], remove.inputs["Geometry"]),
(either.outputs[0], remove.inputs["Selection"]),
(remove.outputs[0], result.inputs[0])):
cut_group.links.new(a, b)
modifier.node_group = cut_group
scene.render.filepath = str(output / "comparison.png")
bpy.ops.render.render(write_still=True)
if capture_camera is not None:
scene.camera = capture_camera
scene.render.filepath = str(output / "capture_view.png")
bpy.ops.render.render(write_still=True)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--verify", action="store_true")
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [])
output = args.output.resolve()
if args.verify:
bpy.ops.wm.open_mainfile(filepath=str(output / "panorama_trial.blend"))
verify(output)
else:
build_scene(output)
if __name__ == "__main__":
main()