"""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()