diff --git a/.gitignore b/.gitignore index e657b0a2..0315f7dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ # User specific editor and IDE configurations .vs -launchSettings.json -!SeeSharp.Templates/content/SeeSharp.Blazor.Template/Properties/launchSettings.json .idea SeeSharp.sln.DotSettings.user diff --git a/BlenderExtension/see_blender/__init__.py b/BlenderExtension/see_blender/__init__.py index 5758292a..47c46164 100644 --- a/BlenderExtension/see_blender/__init__.py +++ b/BlenderExtension/see_blender/__init__.py @@ -1,4 +1,4 @@ -from . import exporter, render_engine, material_ui, material, world +from . import exporter, render_engine, material_ui, material, world, importer def register(): exporter.register() @@ -6,6 +6,7 @@ def register(): material_ui.register() material.register() world.register() + importer.register() def unregister(): exporter.unregister() @@ -13,3 +14,4 @@ def unregister(): material_ui.unregister() material.unregister() world.unregister() + importer.unregister() diff --git a/BlenderExtension/see_blender/exporter.py b/BlenderExtension/see_blender/exporter.py index 00975799..d862f79b 100644 --- a/BlenderExtension/see_blender/exporter.py +++ b/BlenderExtension/see_blender/exporter.py @@ -140,6 +140,8 @@ def material_to_json(material, out_dir): material.emission_color[1] * material.emission_strength, material.emission_color[2] * material.emission_strength )), + "emission_color": map_rgb(material.emission_color), + "emission_strength": material.emission_strength, "emissionIsGlossy": material.emission_is_glossy, "emissionExponent": material.emission_glossy_exponent } diff --git a/BlenderExtension/see_blender/importer.py b/BlenderExtension/see_blender/importer.py new file mode 100644 index 00000000..a1147aae --- /dev/null +++ b/BlenderExtension/see_blender/importer.py @@ -0,0 +1,369 @@ +import os +import json +import math +import bpy +import mathutils +import bmesh +from bpy_extras.io_utils import axis_conversion + +# ------------------------------------------------------------------------ +# Utility +# ------------------------------------------------------------------------ + +def load_image(path): + """Loads image into Blender or returns existing.""" + abspath = bpy.path.abspath(path) + if not os.path.exists(abspath): + print(f"WARNING: Missing texture: {abspath}") + return None + img = bpy.data.images.load(abspath, check_existing=True) + return img + + +def make_material(name, mat_json, base_path): + """Create a Blender material based on SeeSharp material JSON definition.""" + mat = bpy.data.materials.new(name) + mat.use_nodes = True + nt = mat.node_tree + nodes = nt.nodes + links = nt.links + + nodes.clear() + + output = nodes.new("ShaderNodeOutputMaterial") + principled = nodes.new("ShaderNodeBsdfPrincipled") + principled.location = (-200, 0) + output.location = (200, 0) + links.new(principled.outputs["BSDF"], output.inputs["Surface"]) + + # -------- Base color (texture or rgb) + # base_color = mat_json["baseColor"] + base_color = mat_json.get("baseColor") + if base_color: + if base_color["type"] == "rgb": + principled.inputs["Base Color"].default_value = base_color["value"] + [1.0] + elif base_color["type"] == "image": + img_path = os.path.join(base_path, base_color["filename"]) + img = load_image(img_path) + if img: + tex = nodes.new("ShaderNodeTexImage") + tex.image = img + links.new(tex.outputs["Color"], principled.inputs["Base Color"]) + + # -------- Roughness (texture or float) + roughness = mat_json.get("roughness", 1.0) + if isinstance(roughness, str): # texture + img_path = os.path.join(base_path, roughness) + img = load_image(img_path) + if img: + tex = nodes.new("ShaderNodeTexImage") + tex.image = img + tex.location = (-200, -250) + links.new(tex.outputs["Color"], principled.inputs["Roughness"]) + else: + principled.inputs["Roughness"].default_value = float(roughness) + + # Metallic, IOR, Anisotropic + principled.inputs["Metallic"].default_value = mat_json.get("metallic", 0.0) + principled.inputs["IOR"].default_value = mat_json.get("IOR", 1.45) + principled.inputs["Anisotropic"].default_value = mat_json.get("anisotropic", 0.0) + + # Emission + emission_json = mat_json.get("emission") + if emission_json and emission_json.get("type") == "rgb": + # color = mat_json["emission_color"]["value"] + if "emission_color" in mat_json: + color = mat_json["emission_color"].get("value", [1.0, 1.0, 1.0]) + else: + # fallback to emission value itself + color = emission_json.get("value", [0.0, 0.0, 0.0]) + strength = mat_json.get("emission_strength", 0.0) + principled.inputs["Emission Color"].default_value = (*color[:3], 1.0) + principled.inputs["Emission Strength"].default_value = strength + # if mat_json.get("emissionIsGlossy", False): + # principled.inputs["Emission Strength"].default_value = mat_json["emissionExponent"] + + return mat + +def load_mesh(filepath): + ext = os.path.splitext(filepath)[1].lower() + + before = set(bpy.data.objects) + + if ext == ".ply": + bpy.ops.wm.ply_import(filepath=filepath) + + elif ext == ".obj": + bpy.ops.wm.obj_import(filepath=filepath) + + else: + raise RuntimeError(f"Unsupported mesh format: {ext}") + + after = set(bpy.data.objects) + new_objs = list(after - before) + if new_objs: + return new_objs[0] + return None + +# def load_ply(filepath): +# """Load a .ply mesh and return the created object.""" +# before = set(bpy.data.objects) +# bpy.ops.wm.ply_import(filepath=filepath) +# after = set(bpy.data.objects) + +# new_objs = list(after - before) +# if new_objs: +# return new_objs[0] +# return None + +def import_trimesh_object(obj, mat_lookup): + name = obj.get("name", "Trimesh") + + mesh = bpy.data.meshes.new(name) + bm = bmesh.new() + + verts = obj["vertices"] + indices = obj["indices"] + + # ---- vertices + bm_verts = [] + for i in range(0, len(verts), 3): + bm_verts.append(bm.verts.new((verts[i], verts[i+1], verts[i+2]))) + bm.verts.ensure_lookup_table() + + # ---- faces + for i in range(0, len(indices), 3): + try: + bm.faces.new(( + bm_verts[indices[i]], + bm_verts[indices[i+1]], + bm_verts[indices[i+2]], + )) + except ValueError: + pass + + bm.to_mesh(mesh) + bm.free() + + obj_bl = bpy.data.objects.new(name, mesh) + bpy.context.collection.objects.link(obj_bl) + + # ---- normals (optional) + if "normals" in obj: + normals = obj["normals"] + loop_normals = [] + + for loop in mesh.loops: + vi = loop.vertex_index + loop_normals.append(mathutils.Vector(normals[vi*3 : vi*3 + 3])) + + mesh.normals_split_custom_set(loop_normals) + + # ---- UVs (optional) + if "uv" in obj: + uv_layer = mesh.uv_layers.new(name="UVMap") + uvs = obj["uv"] + for poly in mesh.polygons: + for loop_idx in poly.loop_indices: + vi = mesh.loops[loop_idx].vertex_index + uv_layer.data[loop_idx].uv = ( + uvs[vi*2], + uvs[vi*2 + 1] + ) + + # ---- material + mat_name = obj.get("material") + if mat_name in mat_lookup: + mesh.materials.append(mat_lookup[mat_name]) + + return obj_bl + + +# ------------------------------------------------------------------------ +# Camera +# ------------------------------------------------------------------------ + +def import_camera(cam_json, transform_json, scene): + """Recreate SeeSharp camera""" + cam_data = bpy.data.cameras.new("Camera") + cam_obj = bpy.data.objects.new("Camera", cam_data) + scene.collection.objects.link(cam_obj) + scene.camera = cam_obj + + # ------------ Transform + pos = transform_json["position"] + rot = transform_json["rotation"] + + cam_obj.location = (-pos[0], pos[2], pos[1]) + + # inverse Euler mapping + eul = mathutils.Euler(( + math.radians(rot[0] + 90), # x_euler + math.radians(rot[2]), # y_euler + math.radians(rot[1] - 180) # z_euler + ), 'XYZ') + cam_obj.rotation_euler = eul + + # ------------ FOV (vertical → horizontal) + vert_fov = math.radians(cam_json["fov"]) + aspect = scene.render.resolution_y / scene.render.resolution_x + horiz_fov = 2 * math.atan(math.tan(vert_fov / 2) / aspect) + cam_data.angle = horiz_fov + + return cam_obj + + +# ------------------------------------------------------------------------ +# Background HDR +# ------------------------------------------------------------------------ + +def import_background(bg_json, base_path): + world = bpy.context.scene.world + world.use_nodes = True + nt = world.node_tree + + env_tex = nt.nodes.new("ShaderNodeTexEnvironment") + env_tex.location = (-300, 0) + + fname = os.path.join(base_path, bg_json["filename"]) + img = load_image(fname) + if img: + env_tex.image = img + + bg = nt.nodes["Background"] + nt.links.new(env_tex.outputs["Color"], bg.inputs["Color"]) + + +# ------------------------------------------------------------------------ +# Main Import Logic +# ------------------------------------------------------------------------ + +def import_seesharp(filepath): + with open(filepath, "r") as f: + data = json.load(f) + + base_path = os.path.dirname(filepath) + + scene = bpy.context.scene + + # ------------------------------------------------------------------ + # Materials + # ------------------------------------------------------------------ + mat_lookup = {} + if "materials" in data: + for m in data["materials"]: + mat = make_material(m["name"], m, base_path) + mat_lookup[m["name"]] = mat + + # ------------------------------------------------------------------ + # Background + # ------------------------------------------------------------------ + if "background" in data: + import_background(data["background"], base_path) + + # ------------------------------------------------------------------ + # Camera + # ------------------------------------------------------------------ + if "cameras" in data and "transforms" in data: + cam_desc = data["cameras"][0] + transform = next(t for t in data["transforms"] if t["name"] == cam_desc["transform"]) + import_camera(cam_desc, transform, scene) + + # ------------------------------------------------------------------ + # Meshes / Objects + # ------------------------------------------------------------------ + global_matrix = axis_conversion(from_forward="Z", from_up="Y").to_4x4() + + if "objects" in data: + for obj in data["objects"]: + if obj.get("type") == "trimesh": + new_obj = import_trimesh_object(obj, mat_lookup) + else: + # fallback to existing PLY logic + ply_path = os.path.join(base_path, obj["relativePath"]) + new_obj = load_mesh(ply_path) + if not new_obj: + print(f"Failed to load {ply_path}") + continue + + if obj.get("material") in mat_lookup: + new_obj.data.materials.append(mat_lookup[obj["material"]]) + # Apply transform (shared) + new_obj.matrix_world = global_matrix.inverted() + # for obj in data["objects"]: + # # ply_path = os.path.join(base_path, obj["relativePath"]) + # rel_path = obj.get("relativePath") + + # if not rel_path: + # print(f"⚠ Skipping object '{obj.get('name', 'UNKNOWN')}', no relativePath") + # continue + + # ply_path = os.path.join(base_path, rel_path) + # new_obj = load_ply(ply_path) + # if not new_obj: + # print(f"Failed to load {ply_path}") + # continue + + # # Apply SeeSharp → Blender inverse transform + # new_obj.matrix_world = global_matrix.inverted() + + # # Assign material + # if obj.get("material") in mat_lookup: + # new_obj.data.materials.append(mat_lookup[obj["material"]]) + + bpy.context.scene.render.engine = "SEE_SHARP" + + try: + bpy.ops.seesharp.convert_all_materials() + print("✔ Converted all materials to SeeSharp") + except Exception as e: + print("❌ Failed to convert materials:", e) + + print("SeeSharp scene import finished.") + + +# ------------------------------------------------------------------------ +# Blender Operator +# ------------------------------------------------------------------------ +class SeeSharpImporter(bpy.types.Operator): + """Import SeeSharp scene (.json)""" + bl_idname = "import_scene.seesharp" + bl_label = "Import SeeSharp Scene" + + filename_ext = ".json" + filter_glob: bpy.props.StringProperty( + default="*.json", + options={'HIDDEN'} + ) + + filepath: bpy.props.StringProperty( + name="File Path", + description="Path to SeeSharp JSON scene", + maxlen=1024, + subtype='FILE_PATH' + ) + + def execute(self, context): + import_seesharp(self.filepath) + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {'RUNNING_MODAL'} + +def menu_func_import(self, context): + self.layout.operator(SeeSharpImporter.bl_idname, text="SeeSharp Scene (.json)") + + +def register(): + bpy.utils.register_class(SeeSharpImporter) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + + +def unregister(): + bpy.utils.unregister_class(SeeSharpImporter) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/__init__.py b/BlenderExtension/see_blender_link/__init__.py new file mode 100644 index 00000000..fb973e93 --- /dev/null +++ b/BlenderExtension/see_blender_link/__init__.py @@ -0,0 +1,20 @@ +bl_info = { + "name": "SeeBlender Link", + "author": "Minh Nguyen", + "version": (1, 0), + "blender": (4, 5, 0), + "description": "Enable communication between Blender and SeeSharp application", + "category": "System", +} + +from .addons import path_viewer, cursor_tracker + +modules = (path_viewer, cursor_tracker,) + +def register(): + for m in modules: + m.register() + +def unregister(): + for m in reversed(modules): + m.unregister() \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/cursor_tracker/__init__.py b/BlenderExtension/see_blender_link/addons/cursor_tracker/__init__.py new file mode 100644 index 00000000..a8137bee --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/cursor_tracker/__init__.py @@ -0,0 +1,200 @@ +import bpy +import threading +import socket +import time +import json +from mathutils import Vector +from ...config import HOST, PORT_IN, PORT_OUT +from ...transport.sender import send_to_blazor +stop_flag = False +send_thread = None +last_pos = (None, None, None) +# Continuously updated viewport data +current_area = None +current_region = None +current_rv3d = None + +view_handler = None + + +# -------------------------------------------------------------------- +# VIEW TRACKER (runs inside Blender's UI thread on every redraw) +# -------------------------------------------------------------------- + +def update_view_data(): + """ + This function runs every viewport redraw and always updates + the current view matrix, region, and area. + """ + global current_area, current_region, current_rv3d + + # Look through all windows and areas until we find a VIEW_3D + for window in bpy.context.window_manager.windows: + screen = window.screen + if not screen: + continue + + for area in screen.areas: + if area.type == 'VIEW_3D': + region = next((r for r in area.regions if r.type == 'WINDOW'), None) + if not region: + continue + + rv3d = area.spaces.active.region_3d + if rv3d: + current_area = area + current_region = region + current_rv3d = rv3d + return + + +# -------------------------------------------------------------------- +# TCP sending loop (runs in background thread) +# -------------------------------------------------------------------- + +def raycast_and_send_loop(): + global stop_flag, last_pos + + while not stop_flag: + + # Ensure viewport data is available + if not current_rv3d: + time.sleep(0.05) + continue + + try: + scene = bpy.context.scene + cursor_pos = scene.cursor.location.copy() + + # Get ray origin = user view position + view_matrix = current_rv3d.view_matrix + origin = view_matrix.inverted().translation + + direction = (cursor_pos - origin).normalized() + + depsgraph = bpy.context.evaluated_depsgraph_get() + hit, loc, normal, face_idx, obj, _ = scene.ray_cast( + depsgraph, origin, direction + ) + if loc != last_pos: + last_pos = loc + if hit: + data = { + "event": "cursor_tracked", + "object": obj.name, + "hit_position": [round(loc.x, 4), round(loc.y, 4), round(loc.z, 4)], + "normal": [round(normal.x, 4), round(normal.y, 4), round(normal.z, 4)], + "face_index": face_idx, + "cursor_position": [round(cursor_pos.x, 4), round(cursor_pos.y, 4), round(cursor_pos.z, 4)] + } + else: + data = { + "event": "cursor_tracked", + "object": None, + "cursor_position": [round(cursor_pos.x, 4), round(cursor_pos.y, 4), round(cursor_pos.z, 4)] + } + # s.sendall((json.dumps(data) + "\n").encode("utf8")) + send_to_blazor(data) + # print(data) + print("Sending JSON:", json.dumps(data)) + except Exception as e: + print("Error in thread loop:", e) + break + + time.sleep(0.25) + + # s.close() + print("Stopped sending") + + +# -------------------------------------------------------------------- +# Start / Stop Functions +# -------------------------------------------------------------------- + +def start_sender(): + global stop_flag, send_thread + stop_flag = False + + send_thread = threading.Thread(target=raycast_and_send_loop, daemon=True) + send_thread.start() + print("Sender started") + + +def stop_sender(): + global stop_flag + stop_flag = True + print("Sender stopping...") + + +# -------------------------------------------------------------------- +# UI Panel + Property +# -------------------------------------------------------------------- + +def toggle_sender(self, context): + if self.sending_enabled: + start_sender() + else: + stop_sender() + + +class CursorTrackerProperties(bpy.types.PropertyGroup): + sending_enabled: bpy.props.BoolProperty( + name="Send Cursor Info", + description="Enable real-time raycast + cursor output", + default=False, + update=toggle_sender + ) + + +class CursorTrackerPanel(bpy.types.Panel): + bl_label = "Cursor Tracker" + bl_idname = "cursor_tracker_panel" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'SeeSharp' + + def draw(self, context): + layout = self.layout + props = context.scene.cursor_sender_props + layout.prop(props, "sending_enabled") + + +# -------------------------------------------------------------------- +# Registration + View Handler +# -------------------------------------------------------------------- + +classes = ( + CursorTrackerProperties, + CursorTrackerPanel, +) + +def register(): + global view_handler + + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.cursor_sender_props = bpy.props.PointerProperty(type=CursorTrackerProperties) + + # Add draw handler to track view continuously + view_handler = bpy.types.SpaceView3D.draw_handler_add( + update_view_data, (), 'WINDOW', 'POST_VIEW' + ) + print("Add-on registered and view tracking started.") + + +def unregister(): + global view_handler + + stop_sender() + + if view_handler: + bpy.types.SpaceView3D.draw_handler_remove(view_handler, 'WINDOW') + view_handler = None + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.cursor_sender_props + + print("Add-on unregistered.") diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/__init__.py b/BlenderExtension/see_blender_link/addons/path_viewer/__init__.py new file mode 100644 index 00000000..515ecad9 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/__init__.py @@ -0,0 +1,118 @@ +import bpy +from ...transport.receiver import start, stop +from ...core.receiver import Receiver +from .dispatcher import dispatcher +import json + +class EdgeProps(bpy.types.PropertyGroup): + json_data: bpy.props.StringProperty(name="Edge JSON Data") + + +class PathViewerProps(bpy.types.PropertyGroup): + enabled: bpy.props.BoolProperty( + name="Enable Path Viewer", + description="Listen for path viewer command sent from Blazor", + default=False, + update=lambda self, context: toggle_receiver(self, context) + ) + +receiver = Receiver(dispatcher) + +def toggle_receiver(self, context): + if self.enabled: + print(receiver.dispatcher._handlers) + start(receiver) + else: + stop() + +class PathViewerPanel(bpy.types.Panel): + bl_label = "Path Viewer" + bl_idname = "VIEW3D_PT_path_viewer" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "SeeSharp" + + def draw(self, context): + layout = self.layout + props = context.scene.path_viewer_props + layout.prop(props, "enabled") + +def draw_dict(layout, data, level=0): + """Recursively draw any dict or list in Blender UI.""" + if isinstance(data, dict): + for key, value in data.items(): + row = layout.row() + if isinstance(value, (dict, list)): + col = layout.column() + col.label(text=f"{key}:") + box = col.box() + draw_dict(box, value, level + 1) + else: + row.label(text=f"{key}: {value}") + elif isinstance(data, list): + for i, entry in enumerate(data): + col = layout.column() + col.label(text=f"[{i}]") + if isinstance(entry, (dict, list)): + box = col.box() + draw_dict(box, entry, level + 1) + else: + col.label(text=str(entry)) + +class EdgePanel(bpy.types.Panel): + bl_label = "Edge Settings" + bl_idname = "EDGE_DATA_PT_panel" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "data" # IMPORTANT → this puts the panel under Object Data Properties + + @classmethod + def poll(cls, context): + obj = context.object + return obj and obj.get("is_edge") == True + + def draw(self, context): + layout = self.layout + obj = context.object + + json_str = obj.edge_data.json_data + if not json_str: + layout.label(text="No data.") + return + + try: + data = json.loads(json_str) + draw_dict(layout.box(), data) + except Exception as e: + layout.label(text=f"JSON Error: {e}") + +classes = ( + PathViewerProps, + PathViewerPanel, + EdgeProps, + EdgePanel +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.path_viewer_props = bpy.props.PointerProperty( + type=PathViewerProps + ) + + bpy.types.Object.edge_data = bpy.props.PointerProperty(type=EdgeProps) + + print("[BlazorReceiver] Add-on registered") + + +def unregister(): + stop() + del bpy.types.Object.edge_data + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.path_viewer_props + + print("[BlazorReceiver] Add-on unregistered") \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/__init__.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/click_on_node.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/click_on_node.py new file mode 100644 index 00000000..97acdbf7 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/click_on_node.py @@ -0,0 +1,24 @@ +import bpy +import json +from ....transport.sender import send_to_blazor + +def handle_click_on_node(msg): + json_string = msg.get("path") + col_id = msg.get("path_id") + node_list = json.loads(json_string) + is_full_graph = msg.get("is_full_graph") + def run(): + collection = bpy.data.collections[f"arrow_group_{col_id}"] + for obj in collection.objects: + obj.hide_set(True) + if (is_full_graph): + for obj in collection.objects: + obj.hide_set(False) + else: + for i in range(len(node_list) - 1): + var1 = node_list[i] + var2 = node_list[i + 1] + for obj in collection.objects: + if obj.name == f"{var1}-{var2}" or obj.name == f"{var2}-{var1}": + obj.hide_set(False) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py new file mode 100644 index 00000000..06a7fba8 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/create_path.py @@ -0,0 +1,190 @@ +import bpy +from mathutils import Vector +from ....utils.helper import get_scene_scale, renderer_to_blender_world +import json +from ....transport.sender import send_to_blazor +def handle_create_path(msg): + json_string = msg.get("graph") + user_group_id = msg.get("id") + data = json.loads(json_string) + + def flatten_tree(root): + nodes: list[dict] = [] + pairs: list[tuple[str, str]] = [] + visited = set() + + def visit(node: dict): + node_id = node["Id"] + if node_id in visited: + return + visited.add(node_id) + + # collect node + + flat_node = { + k: v for k, v in node.items() + if k != "Successors" + } + nodes.append(flat_node) + # collect parent-child edge (skip roots) + parent_id = node.get("ancestorId") + if parent_id is not None: + pairs.append((parent_id, node_id)) + + # recurse + for child in node.get("Successors", []): + visit(child) + + visit(root) + return nodes, pairs + def create_fixed_arrow(A, B, properties, name="Arrow"): + A = Vector(A) + B = Vector(B) + dir_vec = (B - A) + total_len = dir_vec.length + if total_len < 1e-6: + return None + + dir_n = dir_vec.normalized() + + # ---- fixed-size thickness, but *length = A→B exactly* ---- + scene_scale = get_scene_scale() + + tip_len = scene_scale * 0.03 # fixed tip length (~3% scene) + shaft_rad = scene_scale * 0.0025 # fixed thickness (~0.25%) + tip_rad = scene_scale * 0.008 # tip radius (~0.8%) + + # clamp tip length if segment is short + tip_len = min(tip_len, total_len * 0.4) + + shaft_len = max(total_len - tip_len, total_len * 0.05) + + # ---- place shaft and tip ---- + shaft_loc = A + dir_n * (shaft_len * 0.5) + tip_loc = A + dir_n * (shaft_len + tip_len * 0.5) + + # cleanup + bpy.ops.object.select_all(action='DESELECT') + + bpy.ops.mesh.primitive_cylinder_add( + radius=shaft_rad, depth=shaft_len, location=shaft_loc) + shaft = bpy.context.active_object + + bpy.ops.mesh.primitive_cone_add( + radius1=tip_rad, depth=tip_len, location=tip_loc) + tip = bpy.context.active_object + + # rotate to direction + up = Vector((0,0,1)) + rot_q = up.rotation_difference(dir_n) + for obj in (shaft, tip): + obj.rotation_mode = 'QUATERNION' + obj.rotation_quaternion = rot_q + + # join + shaft.select_set(True) + bpy.context.view_layer.objects.active = shaft + tip.select_set(True) + bpy.ops.object.join() + obj = bpy.context.active_object + obj.name = name + bpy.ops.object.shade_smooth() + + obj.edge_data.json_data = json.dumps(properties, indent=2) + obj["is_edge"] = True + return obj + + def assign_colour(obj, typeA, typeB, base_name): + rgb = (1.0, 0.0, 0.0) + if (typeA == "BSDFSampleNode"): + if (typeB == "BSDFSampleNode"): + rgb = (1.0, 0.0, 0.0) + elif (typeB == "NextEventNode"): + rgb = (0.0, 0.0, 1.0) + elif (typeB == "BackgroundNode"): + rgb = (0.5, 0.0, 0.5) + else: + rgb = (1.0, 0.0, 0.0) + if (typeA == "LightPathNode" or typeB == "LightPathNode"): + rgb = (0.0, 1.0, 0.0) + + # Create material with color + mat = bpy.data.materials.new(name=base_name + "_mat") + mat.use_nodes = True + bsdf = mat.node_tree.nodes.get("Principled BSDF") + if bsdf: + bsdf.inputs["Base Color"].default_value = (rgb[0], rgb[1], rgb[2], 1) + obj.data.materials.append(mat) + return obj + + def edge_type(start, end): + if (end == "BSDFSampleNode"): + return "BSDF" + elif (end == "NextEventNode"): + return "Next Event" + elif (end == "BackgroundNode"): + return "Background" + elif (end == "LightPathNode"): + return "Light Path" + elif (end == "ConnectionNode"): + if (start == "BSDFSampleNode"): + return "Camera Path - Connection" + elif (start == "LightPathNode"): + return "Light Path - Connection" + elif (end == "MergeNode"): + if (start == "BSDFSampleNode"): + return "Camera Path - Merge" + elif (start == "LightPathNode"): + return "Light Path - Merge" + else: + return "Invalid" + + def run(): + col_name = f"arrow_group_{user_group_id}" + col = bpy.data.collections.new(col_name) + bpy.context.scene.collection.children.link(col) + + id_to_node = {} + nodes, pairs = flatten_tree(data) + + for node in nodes: + pos = node["Position"] + id_to_node[node["Id"]] = {"pos": renderer_to_blender_world(Vector((pos["X"], pos["Y"], pos["Z"]))), + "data": node, + "type": node["$type"]} + + for start_id, end_id in pairs: + if (id_to_node[start_id]["type"] == "LightPathNode" or id_to_node[end_id]["type"] == "LightPathNode"): + # reverse the direction if its light path + temp_id = start_id + start_id = end_id + end_id = temp_id + start = id_to_node[start_id]["pos"] #id_to_node.get(start_id) + end = id_to_node[end_id]["pos"] #id_to_node.get(end_id) + #----------------------------------------Add type------------------------------- + arrow_type = edge_type(id_to_node[start_id]["type"], id_to_node[end_id]["type"]) + props = id_to_node[end_id]["data"] + props = dict([("Type", arrow_type), *props.items()]) + #------------------------------------------------------------------------------- + if start is None or end is None: + print(f"Missing node for edge: {start_id} → {end_id}") + continue + + arrow_name = f"Arrow_{start_id}_to_{end_id}" + obj = create_fixed_arrow(start, end, props, f"{start_id}-{end_id}") + obj = assign_colour(obj, id_to_node[start_id]["type"], id_to_node[end_id]["type"], f"{start_id}-{end_id}") + if obj: + col.objects.link(obj) + # Remove from master scene collection (avoid duplicate visible link) + try: + bpy.context.scene.collection.objects.unlink(obj) + except: + pass + + # Tag object with group ID + obj["blazor_arrow_group"] = user_group_id + send_to_blazor({ + "event": "created", + "id": user_group_id + }) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/dbclick_on_node.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/dbclick_on_node.py new file mode 100644 index 00000000..4a176ecf --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/dbclick_on_node.py @@ -0,0 +1,73 @@ +import bpy +import json +from ....transport.sender import send_to_blazor + +def handle_dbclick_on_node(msg): + json_string = msg.get("path") + col_id = msg.get("path_id") + node_list = json.loads(json_string) + is_full_graph = msg.get("is_full_graph") + + def frame_object(obj): + # Ensure Object Mode + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Deselect all + bpy.ops.object.select_all(action='DESELECT') + + # Select and activate object + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # Find a VIEW_3D area + for area in bpy.context.window.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + ): + bpy.ops.view3d.view_selected() + return + + print("No VIEW_3D area found") + + def view_all(center=False): + # Ensure Object Mode (safe for view operators) + if bpy.context.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Find a VIEW_3D area + for area in bpy.context.window.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + with bpy.context.temp_override( + window=bpy.context.window, + area=area, + region=region, + ): + bpy.ops.view3d.view_all(center=center) + return + + print("No VIEW_3D area found") + def run(): + collection = bpy.data.collections[f"arrow_group_{col_id}"] + for obj in collection.objects: + obj.hide_set(True) + if (is_full_graph): + for obj in collection.objects: + obj.hide_set(False) + view_all(center=False) + else: + for i in range(len(node_list) - 1): + var1 = node_list[i] + var2 = node_list[i + 1] + for obj in collection.objects: + if obj.name == f"{var1}-{var2}" or obj.name == f"{var2}-{var1}": + obj.hide_set(False) + frame_object(obj) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/delete_path.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/delete_path.py new file mode 100644 index 00000000..ffc73047 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/delete_path.py @@ -0,0 +1,24 @@ +import bpy +from ....transport.sender import send_to_blazor +def handle_delete_path(msg): + group_id = msg.get("id") + def run(): + col_name = f"arrow_group_{group_id}" + col = bpy.data.collections.get(col_name) + if not col: + print(f"No arrow group found: {col_name}") + return + + # Remove all objects inside + for obj in list(col.objects): + bpy.data.objects.remove(obj, do_unlink=True) + + # Remove the collection itself + bpy.data.collections.remove(col) + + print(f"[ArrowGroup] Deleted group {group_id}") + send_to_blazor({ + "event": "deleted", + "id": group_id + }) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/import_scene.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/import_scene.py new file mode 100644 index 00000000..d3f7dea0 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/import_scene.py @@ -0,0 +1,27 @@ +import bpy + +def handle_import_scene(msg): + file_name = msg.get("scene_name") + def run(): + # clear the current scene + bpy.ops.object.select_all(action='SELECT') + bpy.ops.object.delete() + + # Remove all meshes, materials, images, etc. + for block in bpy.data.meshes: + bpy.data.meshes.remove(block) + for block in bpy.data.materials: + bpy.data.materials.remove(block) + for block in bpy.data.textures: + bpy.data.textures.remove(block) + for block in bpy.data.images: + bpy.data.images.remove(block) + + scene_collection = bpy.context.scene.collection + for col in list(bpy.data.collections): + if col != scene_collection: + bpy.data.collections.remove(col) + + bpy.ops.import_scene.seesharp(filepath=file_name) + + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/commands/select_path.py b/BlenderExtension/see_blender_link/addons/path_viewer/commands/select_path.py new file mode 100644 index 00000000..cbdc66b3 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/commands/select_path.py @@ -0,0 +1,19 @@ +import bpy +from ....transport.sender import send_to_blazor +def handle_select_path(msg): + obj_id = msg.get("id") + col_id = f"arrow_group_{obj_id}" + def run(): + + col = bpy.data.collections.get(col_id) + bpy.ops.object.select_all(action='DESELECT') + # Select all objects in the collection + for obj in col.objects: + obj.select_set(True) + bpy.context.view_layer.objects.active = col.objects[0] if len(col.objects) else None + + send_to_blazor({ + "event": "selected", + "id": obj_id + }) + bpy.app.timers.register(run, first_interval=0) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/addons/path_viewer/dispatcher.py b/BlenderExtension/see_blender_link/addons/path_viewer/dispatcher.py new file mode 100644 index 00000000..dfcaed35 --- /dev/null +++ b/BlenderExtension/see_blender_link/addons/path_viewer/dispatcher.py @@ -0,0 +1,15 @@ +from ...core.dispatcher import Dispatcher +from .commands.create_path import handle_create_path +from .commands.delete_path import handle_delete_path +from .commands.select_path import handle_select_path +from .commands.click_on_node import handle_click_on_node +from .commands.dbclick_on_node import handle_dbclick_on_node +from .commands.import_scene import handle_import_scene + +dispatcher = Dispatcher() +dispatcher.register("create_path", handle_create_path) +dispatcher.register("delete_path", handle_delete_path) +dispatcher.register("select_path", handle_select_path) +dispatcher.register("click_on_node", handle_click_on_node) +dispatcher.register("dbclick_on_node", handle_dbclick_on_node) +dispatcher.register("import_scene", handle_import_scene) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/config.py b/BlenderExtension/see_blender_link/config.py new file mode 100644 index 00000000..885e2919 --- /dev/null +++ b/BlenderExtension/see_blender_link/config.py @@ -0,0 +1,4 @@ +# config.py +HOST = "127.0.0.1" +PORT_IN = 5051 +PORT_OUT = 5052 \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/core/__init__.py b/BlenderExtension/see_blender_link/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/core/dispatcher.py b/BlenderExtension/see_blender_link/core/dispatcher.py new file mode 100644 index 00000000..fe885380 --- /dev/null +++ b/BlenderExtension/see_blender_link/core/dispatcher.py @@ -0,0 +1,26 @@ +class Dispatcher: + def __init__(self): + self._handlers = {} + + def register(self, command: str, handler): + """ + handler: function(msg: dict) + """ + self._handlers.setdefault(command, []).append(handler) + + def dispatch(self, msg: dict): + cmd = msg.get("command") + if not cmd: + print("[Dispatcher] Missing command") + return + + handlers = self._handlers.get(cmd) + if not handlers: + print(f"[Dispatcher] No handlers for '{cmd}'") + return + + for handler in handlers: + try: + handler(msg) + except Exception as e: + print(f"[Dispatcher] Error in handler '{cmd}':", e) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/core/receiver.py b/BlenderExtension/see_blender_link/core/receiver.py new file mode 100644 index 00000000..896962b3 --- /dev/null +++ b/BlenderExtension/see_blender_link/core/receiver.py @@ -0,0 +1,14 @@ +import json + +class Receiver: + def __init__(self, dispatcher): + self.dispatcher = dispatcher + + def handle_message(self, line: str): + if not line.strip(): + return + try: + msg = json.loads(line) + self.dispatcher.dispatch(msg) + except Exception as e: + print("[Receiver] JSON error:", e) \ No newline at end of file diff --git a/BlenderExtension/see_blender_link/transport/__init__.py b/BlenderExtension/see_blender_link/transport/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/transport/receiver.py b/BlenderExtension/see_blender_link/transport/receiver.py new file mode 100644 index 00000000..7dc143ec --- /dev/null +++ b/BlenderExtension/see_blender_link/transport/receiver.py @@ -0,0 +1,75 @@ +import socket +import threading +import select +from ..config import HOST, PORT_IN +from ..core.receiver import Receiver + +_receiver_thread = None +_stop = False +_server_socket = None + +def _receiver_loop(receiver: Receiver): + global _server_socket, _stop + try: + _server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + _server_socket.bind((HOST, PORT_IN)) + _server_socket.listen(1) + _server_socket.setblocking(False) + print(f"[Receiver] Listening on {HOST}:{PORT_IN}") + except Exception as e: + print("[Receiver] Bind failed:", e) + return + + buffer = "" + + while not _stop: + try: + readable, _, _ = select.select([_server_socket], [], [], 0.1) + if _server_socket in readable: + try: + conn, addr = _server_socket.accept() + conn.setblocking(False) + print("[Receiver] Connected:", addr) + + except Exception: + continue + + while not _stop: + try: + r, _, _ = select.select([conn], [], [], 0.05) + if conn in r: + chunk = conn.recv(1024) + if not chunk: + break + buffer += chunk.decode() + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + receiver.handle_message(line) + except Exception: + break + conn.close() + except Exception as e: + print("[Receiver] Loop error:", e) + print("[Receiver] Closing...") + try: + _server_socket.close() + except: + pass + _server_socket = None + + +def start(receiver: Receiver): + global _receiver_thread, _stop + if _receiver_thread and _receiver_thread.is_alive(): + return + _stop = False + _receiver_thread = threading.Thread(target=_receiver_loop, args=(receiver,) ,daemon=True) + _receiver_thread.start() + + +def stop(): + global _stop, _server_socket + _stop = True + if _server_socket: + _server_socket.close() diff --git a/BlenderExtension/see_blender_link/transport/sender.py b/BlenderExtension/see_blender_link/transport/sender.py new file mode 100644 index 00000000..7a69cce5 --- /dev/null +++ b/BlenderExtension/see_blender_link/transport/sender.py @@ -0,0 +1,30 @@ +import socket +import json +from ..config import HOST, PORT_OUT + +_blazor_socket = None + +def _connect(): + global _blazor_socket + try: + _blazor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + _blazor_socket.connect((HOST, PORT_OUT)) + print("[Blender->Blazor] Connected") + except Exception as e: + print("[Blender->Blazor] Connect failed:", e) + _blazor_socket = None + + +def send_to_blazor(data: dict): + global _blazor_socket + + if not _blazor_socket: + _connect() + if not _blazor_socket: + return + + try: + msg = json.dumps(data) + "\n" + _blazor_socket.sendall(msg.encode("utf8")) + except Exception: + _blazor_socket = None diff --git a/BlenderExtension/see_blender_link/utils/__init__.py b/BlenderExtension/see_blender_link/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/BlenderExtension/see_blender_link/utils/helper.py b/BlenderExtension/see_blender_link/utils/helper.py new file mode 100644 index 00000000..b9e7405e --- /dev/null +++ b/BlenderExtension/see_blender_link/utils/helper.py @@ -0,0 +1,28 @@ +import bpy +from mathutils import Vector +from bpy_extras.io_utils import axis_conversion + +def get_scene_scale(): + """Return diagonal of bounding box of all mesh objects.""" + meshes = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH'] + if not meshes: + return 1.0 + + min_v = Vector((1e10, 1e10, 1e10)) + max_v = Vector((-1e10, -1e10, -1e10)) + + for obj in meshes: + for v in obj.bound_box: + w = obj.matrix_world @ Vector(v) + min_v = Vector((min(min_v[i], w[i]) for i in range(3))) + max_v = Vector((max(max_v[i], w[i]) for i in range(3))) + + return (max_v - min_v).length + +def renderer_to_blender_world(hit_render_pos): + # This must match your export axis conversion + global_matrix = axis_conversion( + to_forward="Z", + to_up="Y", + ).to_4x4() + return global_matrix.inverted() @ Vector(hit_render_pos) \ No newline at end of file diff --git a/Data/Scenes/CornellBox/CornellBox.json b/Data/Scenes/CornellBox/CornellBox.json index 40b19015..ca726cfd 100644 --- a/Data/Scenes/CornellBox/CornellBox.json +++ b/Data/Scenes/CornellBox/CornellBox.json @@ -1,880 +1,1103 @@ { - "name": "Cornell Box", - "transforms": [ - { - "name": "camera", - "position": [ 0.0, 1.0, 6.8 ], - "rotation": [ 0.0, 0.0, 0.0 ], - "scale": [ 1.0, 1.0, 1.0 ] - } - ], - "cameras": [ - { - "name": "default", - "type": "perspective", - "fov": 19.5, - "transform": "camera" - } - ], - "materials": [ - { - "name": "LeftWall", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.63, 0.065, 0.05] - } - }, - { - "name": "RightWall", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.14, 0.45, 0.091] - } - }, - { - "name": "Floor", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } - }, - { - "name": "Ceiling", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } - }, - { - "name": "BackWall", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } - }, - { - "name": "ShortBox", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } + "name": "Cornell Box", + "transforms": [ + { + "name": "camera", + "position": [ + 0.0, + 1.0, + 6.8 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ], + "scale": [ + 1.0, + 1.0, + 1.0 + ] + } + ], + "cameras": [ + { + "name": "default", + "type": "perspective", + "fov": 19.5, + "transform": "camera" + } + ], + "materials": [ + { + "name": "LeftWall", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.63, + 0.065, + 0.05 + ] }, - { - "name": "TallBox", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [0.725, 0.71, 0.68] - } + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] }, - { - "name": "Light", - "type": "diffuse", - "baseColor": { - "type": "rgb", - "value": [ 0.0, 0.0, 0.0] - } - } - ], - "objects": [ - { - "name": "mesh0", - "emission": { - "type": "rgb", - "unit": "radiance", - "value": [17.0, 12.0, 4.0] - }, - "material": "Light", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -0.24, - 1.98, - -0.22, - 0.23, - 1.98, - -0.22, - 0.23, - 1.98, - 0.16, - -0.24, - 1.98, - 0.16 - ], - "normals": [ - -8.74228e-08, - -1.0, - 1.86006e-07, - -8.74228e-08, - -1.0, - 1.86006e-07, - -8.74228e-08, - -1.0, - 1.86006e-07, - -8.74228e-08, - -1.0, - 1.86006e-07 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh1", - "material": "Floor", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -1.0, - 1.74846e-07, - -1.0, - -1.0, - 1.74846e-07, - 1.0, - 1.0, - -1.74846e-07, - 1.0, - 1.0, - -1.74846e-07, - -1.0 - ], - "normals": [ - 4.37114e-08, - 1.0, - 1.91069e-15, - 4.37114e-08, - 1.0, - 1.91069e-15, - 4.37114e-08, - 1.0, - 1.91069e-15, - 4.37114e-08, - 1.0, - 1.91069e-15 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0 + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "RightWall", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.14, + 0.45, + 0.091 ] }, - { - "name": "mesh2", - "material": "Ceiling", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - 1.0, - 2.0, - 1.0, - -1.0, - 2.0, - 1.0, - -1.0, - 2.0, - -1.0, - 1.0, - 2.0, - -1.0 - ], - "normals": [ - -8.74228e-08, - -1.0, - -4.37114e-08, - -8.74228e-08, - -1.0, - -4.37114e-08, - -8.74228e-08, - -1.0, - -4.37114e-08, - -8.74228e-08, - -1.0, - -4.37114e-08 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0 + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 ] }, - { - "name": "mesh3", - "material": "BackWall", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -1.0, - 0.0, - -1.0, - -1.0, - 2.0, - -1.0, + "emission_color": { + "type": "rgb", + "value": [ 1.0, - 2.0, - -1.0, 1.0, - 0.0, - -1.0 - ], - "normals": [ - 8.74228e-08, - -4.37114e-08, - -1.0, - 8.74228e-08, - -4.37114e-08, - -1.0, - 8.74228e-08, - -4.37114e-08, - -1.0, - 8.74228e-08, - -4.37114e-08, - -1.0 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh4", - "material": "RightWall", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - 1.0, - 0.0, - -1.0, - 1.0, - 2.0, - -1.0, - 1.0, - 2.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "Floor", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 - ], - "normals": [ - 1.0, - -4.37114e-08, - 1.31134e-07, - 1.0, - -4.37114e-08, - 1.31134e-07, - 1.0, - -4.37114e-08, - 1.31134e-07, - 1.0, - -4.37114e-08, - 1.31134e-07 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, + ] + }, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "Ceiling", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh5", - "material": "LeftWall", - "type": "trimesh", - "indices": [ - 0, - 1, - 2, - 0, - 2, - 3 - ], - "vertices": [ - -1.0, - 0.0, - 1.0, - -1.0, - 2.0, - 1.0, - -1.0, - 2.0, - -1.0, - -1.0, - 0.0, - -1.0 - ], - "normals": [ - -1.0, - -4.37114e-08, - -4.37114e-08, - -1.0, - -4.37114e-08, - -4.37114e-08, - -1.0, - -4.37114e-08, - -4.37114e-08, - -1.0, - -4.37114e-08, - -4.37114e-08 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "BackWall", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh6", - "material": "ShortBox", - "type": "trimesh", - "indices": [ - 0, - 2, - 1, - 0, - 3, - 2, - 4, - 6, - 5, - 4, - 7, - 6, - 8, - 10, - 9, - 8, - 11, - 10, - 12, - 14, - 13, - 12, - 15, - 14, - 16, - 18, - 17, - 16, - 19, - 18, - 20, - 22, - 21, - 20, - 23, - 22 - ], - "vertices": [ - -0.0460751, - 0.6, - 0.573007, - -0.0460751, - -2.98023e-08, - 0.573007, - 0.124253, - 0.0, - 0.00310463, - 0.124253, - 0.6, - 0.00310463, - 0.533009, - 0.0, - 0.746079, - 0.533009, - 0.6, - 0.746079, - 0.703337, - 0.6, - 0.176177, - 0.703337, - 2.98023e-08, - 0.176177, - 0.533009, - 0.6, - 0.746079, - -0.0460751, - 0.6, - 0.573007, - 0.124253, - 0.6, - 0.00310463, - 0.703337, - 0.6, - 0.176177, - 0.703337, - 2.98023e-08, - 0.176177, - 0.124253, - 0.0, - 0.00310463, - -0.0460751, - -2.98023e-08, - 0.573007, - 0.533009, - 0.0, - 0.746079, - 0.533009, - 0.0, - 0.746079, - -0.0460751, - -2.98023e-08, - 0.573007, - -0.0460751, - 0.6, - 0.573007, - 0.533009, - 0.6, - 0.746079, - 0.703337, - 0.6, - 0.176177, - 0.124253, - 0.6, - 0.00310463, - 0.124253, - 0.0, - 0.00310463, - 0.703337, - 2.98023e-08, - 0.176177 - ], - "normals": [ - -0.958123, - -4.18809e-08, - -0.286357, - -0.958123, - -4.18809e-08, - -0.286357, - -0.958123, - -4.18809e-08, - -0.286357, - -0.958123, - -4.18809e-08, - -0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - 0.958123, - 4.18809e-08, - 0.286357, - -4.37114e-08, - 1.0, - -1.91069e-15, - -4.37114e-08, - 1.0, - -1.91069e-15, - -4.37114e-08, - 1.0, - -1.91069e-15, - -4.37114e-08, - 1.0, - -1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - 4.37114e-08, - -1.0, - 1.91069e-15, - -0.286357, - -1.25171e-08, - 0.958123, - -0.286357, - -1.25171e-08, - 0.958123, - -0.286357, - -1.25171e-08, - 0.958123, - -0.286357, - -1.25171e-08, - 0.958123, - 0.286357, - 1.25171e-08, - -0.958123, - 0.286357, - 1.25171e-08, - -0.958123, - 0.286357, - 1.25171e-08, - -0.958123, - 0.286357, - 1.25171e-08, - -0.958123 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "ShortBox", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + } + }, + { + "name": "TallBox", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.725, + 0.71, + 0.68 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] }, - { - "name": "mesh7", - "material": "TallBox", - "type": "trimesh", - "indices": [ - 0, - 2, - 1, - 0, - 3, - 2, - 4, - 6, - 5, - 4, - 7, - 6, - 8, - 10, - 9, - 8, - 11, - 10, - 12, - 14, - 13, - 12, - 15, - 14, - 16, - 18, - 17, - 16, - 19, - 18, - 20, - 22, - 21, - 20, - 23, - 22 - ], - "vertices": [ - -0.720444, - 1.2, - -0.473882, - -0.720444, - 0.0, - -0.473882, - -0.146892, - 0.0, - -0.673479, - -0.146892, - 1.2, - -0.673479, - -0.523986, - 0.0, - 0.0906493, - -0.523986, - 1.2, - 0.0906492, - 0.0495656, - 1.2, - -0.108948, - 0.0495656, - 0.0, - -0.108948, - -0.523986, - 1.2, - 0.0906492, - -0.720444, - 1.2, - -0.473882, - -0.146892, - 1.2, - -0.673479, - 0.0495656, - 1.2, - -0.108948, - 0.0495656, - 0.0, - -0.108948, - -0.146892, - 0.0, - -0.673479, - -0.720444, - 0.0, - -0.473882, - -0.523986, - 0.0, - 0.0906493, - -0.523986, - 0.0, - 0.0906493, - -0.720444, - 0.0, - -0.473882, - -0.720444, - 1.2, - -0.473882, - -0.523986, - 1.2, - 0.0906492, - 0.0495656, - 1.2, - -0.108948, - -0.146892, - 1.2, - -0.673479, - -0.146892, - 0.0, - -0.673479, - 0.0495656, - 0.0, - -0.108948 - ], - "normals": [ - -0.328669, - -4.1283e-08, - -0.944445, - -0.328669, - -4.1283e-08, - -0.944445, - -0.328669, - -4.1283e-08, - -0.944445, - -0.328669, - -4.1283e-08, - -0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 0.328669, - 4.1283e-08, - 0.944445, - 3.82137e-15, - 1.0, - -4.37114e-08, - 3.82137e-15, - 1.0, - -4.37114e-08, - 3.82137e-15, - 1.0, - -4.37114e-08, - 3.82137e-15, - 1.0, - -4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -3.82137e-15, - -1.0, - 4.37114e-08, - -0.944445, - 1.43666e-08, - 0.328669, - -0.944445, - 1.43666e-08, - 0.328669, - -0.944445, - 1.43666e-08, - 0.328669, - -0.944445, - 1.43666e-08, - 0.328669, - 0.944445, - -1.43666e-08, - -0.328669, - 0.944445, - -1.43666e-08, - -0.328669, - 0.944445, - -1.43666e-08, - -0.328669, - 0.944445, - -1.43666e-08, - -0.328669 - ], - "uv": [ - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 0.0, - 0.0, - 1.0, - 0.0, + "emission_strength": 0.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + }, + { + "name": "Light", + "type": "generic", + "baseColor": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "roughness": 1.0, + "anisotropic": 0.0, + "IOR": 1.4500000476837158, + "metallic": 0.0, + "specularTintStrength": 0.0, + "specularTransmittance": 0.0, + "emission": { + "type": "rgb", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "emission_color": { + "type": "rgb", + "value": [ 1.0, 1.0, - 0.0, 1.0 ] - } - ] - } \ No newline at end of file + }, + "emission_strength": 10.0, + "emissionIsGlossy": false, + "emissionExponent": 20.0 + } + ], + "objects": [ + { + "name": "mesh0", + "emission": { + "type": "rgb", + "unit": "radiance", + "value": [ + 17.0, + 12.0, + 4.0 + ] + }, + "material": "Light", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -0.24, + 1.98, + -0.22, + 0.23, + 1.98, + -0.22, + 0.23, + 1.98, + 0.16, + -0.24, + 1.98, + 0.16 + ], + "normals": [ + -8.74228e-08, + -1.0, + 1.86006e-07, + -8.74228e-08, + -1.0, + 1.86006e-07, + -8.74228e-08, + -1.0, + 1.86006e-07, + -8.74228e-08, + -1.0, + 1.86006e-07 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh1", + "material": "Floor", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -1.0, + 1.74846e-07, + -1.0, + -1.0, + 1.74846e-07, + 1.0, + 1.0, + -1.74846e-07, + 1.0, + 1.0, + -1.74846e-07, + -1.0 + ], + "normals": [ + 4.37114e-08, + 1.0, + 1.91069e-15, + 4.37114e-08, + 1.0, + 1.91069e-15, + 4.37114e-08, + 1.0, + 1.91069e-15, + 4.37114e-08, + 1.0, + 1.91069e-15 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh2", + "material": "Ceiling", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + 1.0, + 2.0, + 1.0, + -1.0, + 2.0, + 1.0, + -1.0, + 2.0, + -1.0, + 1.0, + 2.0, + -1.0 + ], + "normals": [ + -8.74228e-08, + -1.0, + -4.37114e-08, + -8.74228e-08, + -1.0, + -4.37114e-08, + -8.74228e-08, + -1.0, + -4.37114e-08, + -8.74228e-08, + -1.0, + -4.37114e-08 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh3", + "material": "BackWall", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -1.0, + 0.0, + -1.0, + -1.0, + 2.0, + -1.0, + 1.0, + 2.0, + -1.0, + 1.0, + 0.0, + -1.0 + ], + "normals": [ + 8.74228e-08, + -4.37114e-08, + -1.0, + 8.74228e-08, + -4.37114e-08, + -1.0, + 8.74228e-08, + -4.37114e-08, + -1.0, + 8.74228e-08, + -4.37114e-08, + -1.0 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh4", + "material": "RightWall", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + 1.0, + 0.0, + -1.0, + 1.0, + 2.0, + -1.0, + 1.0, + 2.0, + 1.0, + 1.0, + 0.0, + 1.0 + ], + "normals": [ + 1.0, + -4.37114e-08, + 1.31134e-07, + 1.0, + -4.37114e-08, + 1.31134e-07, + 1.0, + -4.37114e-08, + 1.31134e-07, + 1.0, + -4.37114e-08, + 1.31134e-07 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh5", + "material": "LeftWall", + "type": "trimesh", + "indices": [ + 0, + 1, + 2, + 0, + 2, + 3 + ], + "vertices": [ + -1.0, + 0.0, + 1.0, + -1.0, + 2.0, + 1.0, + -1.0, + 2.0, + -1.0, + -1.0, + 0.0, + -1.0 + ], + "normals": [ + -1.0, + -4.37114e-08, + -4.37114e-08, + -1.0, + -4.37114e-08, + -4.37114e-08, + -1.0, + -4.37114e-08, + -4.37114e-08, + -1.0, + -4.37114e-08, + -4.37114e-08 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh6", + "material": "ShortBox", + "type": "trimesh", + "indices": [ + 0, + 2, + 1, + 0, + 3, + 2, + 4, + 6, + 5, + 4, + 7, + 6, + 8, + 10, + 9, + 8, + 11, + 10, + 12, + 14, + 13, + 12, + 15, + 14, + 16, + 18, + 17, + 16, + 19, + 18, + 20, + 22, + 21, + 20, + 23, + 22 + ], + "vertices": [ + -0.0460751, + 0.6, + 0.573007, + -0.0460751, + -2.98023e-08, + 0.573007, + 0.124253, + 0.0, + 0.00310463, + 0.124253, + 0.6, + 0.00310463, + 0.533009, + 0.0, + 0.746079, + 0.533009, + 0.6, + 0.746079, + 0.703337, + 0.6, + 0.176177, + 0.703337, + 2.98023e-08, + 0.176177, + 0.533009, + 0.6, + 0.746079, + -0.0460751, + 0.6, + 0.573007, + 0.124253, + 0.6, + 0.00310463, + 0.703337, + 0.6, + 0.176177, + 0.703337, + 2.98023e-08, + 0.176177, + 0.124253, + 0.0, + 0.00310463, + -0.0460751, + -2.98023e-08, + 0.573007, + 0.533009, + 0.0, + 0.746079, + 0.533009, + 0.0, + 0.746079, + -0.0460751, + -2.98023e-08, + 0.573007, + -0.0460751, + 0.6, + 0.573007, + 0.533009, + 0.6, + 0.746079, + 0.703337, + 0.6, + 0.176177, + 0.124253, + 0.6, + 0.00310463, + 0.124253, + 0.0, + 0.00310463, + 0.703337, + 2.98023e-08, + 0.176177 + ], + "normals": [ + -0.958123, + -4.18809e-08, + -0.286357, + -0.958123, + -4.18809e-08, + -0.286357, + -0.958123, + -4.18809e-08, + -0.286357, + -0.958123, + -4.18809e-08, + -0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + 0.958123, + 4.18809e-08, + 0.286357, + -4.37114e-08, + 1.0, + -1.91069e-15, + -4.37114e-08, + 1.0, + -1.91069e-15, + -4.37114e-08, + 1.0, + -1.91069e-15, + -4.37114e-08, + 1.0, + -1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + 4.37114e-08, + -1.0, + 1.91069e-15, + -0.286357, + -1.25171e-08, + 0.958123, + -0.286357, + -1.25171e-08, + 0.958123, + -0.286357, + -1.25171e-08, + 0.958123, + -0.286357, + -1.25171e-08, + 0.958123, + 0.286357, + 1.25171e-08, + -0.958123, + 0.286357, + 1.25171e-08, + -0.958123, + 0.286357, + 1.25171e-08, + -0.958123, + 0.286357, + 1.25171e-08, + -0.958123 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + }, + { + "name": "mesh7", + "material": "TallBox", + "type": "trimesh", + "indices": [ + 0, + 2, + 1, + 0, + 3, + 2, + 4, + 6, + 5, + 4, + 7, + 6, + 8, + 10, + 9, + 8, + 11, + 10, + 12, + 14, + 13, + 12, + 15, + 14, + 16, + 18, + 17, + 16, + 19, + 18, + 20, + 22, + 21, + 20, + 23, + 22 + ], + "vertices": [ + -0.720444, + 1.2, + -0.473882, + -0.720444, + 0.0, + -0.473882, + -0.146892, + 0.0, + -0.673479, + -0.146892, + 1.2, + -0.673479, + -0.523986, + 0.0, + 0.0906493, + -0.523986, + 1.2, + 0.0906492, + 0.0495656, + 1.2, + -0.108948, + 0.0495656, + 0.0, + -0.108948, + -0.523986, + 1.2, + 0.0906492, + -0.720444, + 1.2, + -0.473882, + -0.146892, + 1.2, + -0.673479, + 0.0495656, + 1.2, + -0.108948, + 0.0495656, + 0.0, + -0.108948, + -0.146892, + 0.0, + -0.673479, + -0.720444, + 0.0, + -0.473882, + -0.523986, + 0.0, + 0.0906493, + -0.523986, + 0.0, + 0.0906493, + -0.720444, + 0.0, + -0.473882, + -0.720444, + 1.2, + -0.473882, + -0.523986, + 1.2, + 0.0906492, + 0.0495656, + 1.2, + -0.108948, + -0.146892, + 1.2, + -0.673479, + -0.146892, + 0.0, + -0.673479, + 0.0495656, + 0.0, + -0.108948 + ], + "normals": [ + -0.328669, + -4.1283e-08, + -0.944445, + -0.328669, + -4.1283e-08, + -0.944445, + -0.328669, + -4.1283e-08, + -0.944445, + -0.328669, + -4.1283e-08, + -0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 0.328669, + 4.1283e-08, + 0.944445, + 3.82137e-15, + 1.0, + -4.37114e-08, + 3.82137e-15, + 1.0, + -4.37114e-08, + 3.82137e-15, + 1.0, + -4.37114e-08, + 3.82137e-15, + 1.0, + -4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -3.82137e-15, + -1.0, + 4.37114e-08, + -0.944445, + 1.43666e-08, + 0.328669, + -0.944445, + 1.43666e-08, + 0.328669, + -0.944445, + 1.43666e-08, + 0.328669, + -0.944445, + 1.43666e-08, + 0.328669, + 0.944445, + -1.43666e-08, + -0.328669, + 0.944445, + -1.43666e-08, + -0.328669, + 0.944445, + -1.43666e-08, + -0.328669, + 0.944445, + -1.43666e-08, + -0.328669 + ], + "uv": [ + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0 + ] + } + ] +} \ No newline at end of file diff --git a/Examples/BlenderSync/App.razor b/Examples/BlenderSync/App.razor new file mode 100644 index 00000000..6fd3ed1b --- /dev/null +++ b/Examples/BlenderSync/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/Examples/BlenderSync/BlenderSync.csproj b/Examples/BlenderSync/BlenderSync.csproj new file mode 100644 index 00000000..3f177c5f --- /dev/null +++ b/Examples/BlenderSync/BlenderSync.csproj @@ -0,0 +1,18 @@ + + + + Exe + net9.0 + enable + + + + + + + + + + + + diff --git a/SeeSharp.Examples/Imports.cs b/Examples/BlenderSync/Imports.cs similarity index 92% rename from SeeSharp.Examples/Imports.cs rename to Examples/BlenderSync/Imports.cs index 0162e168..dee65517 100644 --- a/SeeSharp.Examples/Imports.cs +++ b/Examples/BlenderSync/Imports.cs @@ -29,4 +29,6 @@ global using SeeSharp.Shading; global using SeeSharp.Shading.Background; global using SeeSharp.Shading.Emitters; -global using SeeSharp.Shading.Materials; \ No newline at end of file +global using SeeSharp.Shading.Materials; + +global using SeeSharp.Blazor; diff --git a/Examples/BlenderSync/MainLayout.razor b/Examples/BlenderSync/MainLayout.razor new file mode 100644 index 00000000..a5af3489 --- /dev/null +++ b/Examples/BlenderSync/MainLayout.razor @@ -0,0 +1,3 @@ +@inherits LayoutComponentBase + +
@Body
diff --git a/Examples/BlenderSync/Pages/BlenderImporter.razor b/Examples/BlenderSync/Pages/BlenderImporter.razor new file mode 100644 index 00000000..829b7cbd --- /dev/null +++ b/Examples/BlenderSync/Pages/BlenderImporter.razor @@ -0,0 +1,118 @@ +@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage +@inject ProtectedSessionStorage ProtectedSessionStore +@using SeeSharp.Experiments +@using SeeSharp.Blazor +@using System; +@using System.IO; + +
+

Select a scene

+ +
+ +
+ @if (isSceneNameValid && !loading && scene?.Name != sceneNameInput.Text) + { + + } + else + { + + + @if (!loading) + { + + } + } +
+
+ +
+ Available scenes + +
+ + @if (loading) + { +

Loading...

+ } + else if (!string.IsNullOrEmpty(scene?.Name)) + { +

Loaded "@(scene.Name)"

+ } + +
+ +@code { + [Parameter] + public EventCallback OnSceneLoaded { get; set; } + + + IEnumerable availableSceneNames + { + get + { + if (_availableSceneNames == null) + _availableSceneNames = SceneRegistry.FindAvailableScenes().Order(); + return _availableSceneNames; + } + } + IEnumerable _availableSceneNames; + + public AutocompleteInput sceneNameInput { get; set; } + + SceneFromFile scene; + bool loading = false; + + SceneFromFile Scene => scene; + + bool isSceneNameValid => sceneNameInput?.Valid == true; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var result = await ProtectedSessionStore.GetAsync("lastScene"); + if (result.Success) + sceneNameInput.Text = result.Value; + } + } + + async Task OnSceneNameUpdate(string newName) + { + await ProtectedSessionStore.SetAsync("lastScene", newName); + } + + async Task LoadScene() + { + if (!isSceneNameValid || loading) return; + loading = true; + await Task.Run(() => scene = SceneRegistry.LoadScene(sceneNameInput.Text)); + loading = false; + await OnSceneLoaded.InvokeAsync(scene); + } + + public string FindJson(string folder) + { + folder = "../../Data/Scenes/" + folder; + folder = Path.GetFullPath(folder); + var files = Directory.GetFiles(folder, "*.json"); + if (files.Length == 0) + { + Console.WriteLine("Error: No JSON file found in the folder."); + return null; + } + + return Path.GetFullPath(files[0]); + } +} diff --git a/Examples/BlenderSync/Pages/BlenderImporter.razor.css b/Examples/BlenderSync/Pages/BlenderImporter.razor.css new file mode 100644 index 00000000..7dd987e3 --- /dev/null +++ b/Examples/BlenderSync/Pages/BlenderImporter.razor.css @@ -0,0 +1,66 @@ +::deep .scene-button { + margin-top: 0px; + margin-bottom: 2px; + background-color: #cfe8ef; + color: rgb(12, 12, 12); + padding-left: 4px; + padding-top: 2px; + padding-bottom: 2px; + overflow: hidden; +} + +::deep .scene-button:hover { + cursor: pointer; + background-color: #8edeee; +} + +.dropdown-header { + font-weight: bold; +} + +.dropdown-header:hover { + cursor: pointer; +} + +.dropdown-body { + column-count: auto; + column-width: 12rem; + margin-left: 8px; + border-left-width: 8px; + border-left-color: #208dab; + border-left-style: solid; + background-color: #cfe8ef; +} + +.scene-picker { + background-color: #e6f1f4; + padding: 8px; + + border-style: solid; + border-width: 1pt; + border-color: #2b3f44; +} + +.btn { + background-color: #a4e1f2; + border-style: none; + /* border-width: 2px; + border-color: #245e6f; */ + color: black; + font-size: medium; + padding-left: 8px; + padding-right: 8px; + padding-bottom: 4px; + padding-top: 4px; +} + +.btn:hover { + background-color: #c9eff4; + cursor: pointer; +} + +.btn:disabled { + background-color: #e5f1f5; + color: #96b4bd; + border-color: #96b4bd; +} \ No newline at end of file diff --git a/Examples/BlenderSync/Pages/CursorTracker.razor b/Examples/BlenderSync/Pages/CursorTracker.razor new file mode 100644 index 00000000..91bce976 --- /dev/null +++ b/Examples/BlenderSync/Pages/CursorTracker.razor @@ -0,0 +1,44 @@ +@page "/CursorTracker" +@using SeeSharp.Blender +@inject SeeSharp.Blender.CursorTrackerClient Client +@inject SeeSharp.Blender.BlenderCommandSender Commander +@inject SeeSharp.Blender.BlenderEventListener Listener +

Blender Live Data

+ +@if (_data is null) +{ +

Waiting for Blender...

+} +else +{ +
+ @if (_data.Hit_Position != null) + { +

Cursor: @Format(_data.Cursor_Position)

+

Object: @_data.Object

+

Hit Position: @Format(_data.Hit_Position)

+

Normal: @Format(_data.Normal)

+

Face Index: @_data.Face_Index

+ } + else + { +

No object hit

+ } +
+} + +@code { + private BlenderCursorData? _data; + protected override void OnInitialized() + { + Client._cursor_tracked.OnCursorTracked += data => + { + Console.WriteLine("RAW JSON: " + JsonSerializer.Serialize(data)); + _data = data; + InvokeAsync(StateHasChanged); + }; + } + + string Format(float[]? arr) + => arr == null ? "null" : string.Join(", ", arr); +} \ No newline at end of file diff --git a/Examples/BlenderSync/Pages/Experiment.razor b/Examples/BlenderSync/Pages/Experiment.razor new file mode 100644 index 00000000..76543c49 --- /dev/null +++ b/Examples/BlenderSync/Pages/Experiment.razor @@ -0,0 +1,97 @@ +@using SeeSharp.Experiments +@using SeeSharp +@using SeeSharp.Blazor + +@inject IJSRuntime JS + +@page "/Experiment" + +

Example experiment

+ +
+
+ +
+ +
+ +
+
+ @if (readyToRun) + { +

+ } + + + +
+ + @if (!running) + { + @if (resultsAvailable) + { +
+ + + @if (selected.HasValue && selected.Value) + { + + + + + +
Mesh@(selected.Value.Mesh.Name)
Material@(selected.Value.Mesh.Material.Name) (roughness: @(selected.Value.Mesh.Material.GetRoughness(selected.Value)), transmissive: @(selected.Value.Mesh.Material.IsTransmissive(selected.Value)))
Distance@(selected.Value.Distance)
Position@(selected.Value.Position)
+ } + +
+ } + } + else + { +

Rendering...

+ } +
+ + + +@code { + SceneSelector sceneSelector; + Scene scene; + bool readyToRun = false; + bool running = false; + bool sceneJustLoaded = false; + bool resultsAvailable = false; + ElementReference runButton; + + SimpleImageIO.FlipBook flip; + + async Task OnSceneLoaded(SceneFromFile sceneFromFile) + { + await Task.Run(() => scene = sceneFromFile.MakeScene()); + flip = null; + resultsAvailable = false; + readyToRun = true; + sceneJustLoaded = true; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (readyToRun && sceneJustLoaded) + { + await runButton.FocusAsync(); + } + + sceneJustLoaded = false; + } + + async Task OnRunClick() + { + readyToRun = false; + resultsAvailable = false; + running = true; + await Task.Run(() => RunExperiment()); + readyToRun = true; + running = false; + resultsAvailable = true; + } +} \ No newline at end of file diff --git a/Examples/BlenderSync/Pages/Experiment.razor.cs b/Examples/BlenderSync/Pages/Experiment.razor.cs new file mode 100644 index 00000000..2ee7003f --- /dev/null +++ b/Examples/BlenderSync/Pages/Experiment.razor.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Components; + +namespace BlenderSync.Pages; + +public partial class Experiment : ComponentBase +{ + const int Width = 1280; + const int Height = 720; + const int MaxDepth = 10; + + int NumSamples = 2; + + long renderTimePT, renderTimeVCM; + + void RunExperiment() + { + flip = new FlipBook(660, 580) + .SetZoom(FlipBook.InitialZoom.FillWidth) + .SetToneMapper(FlipBook.InitialTMO.Exposure(scene.RecommendedExposure)) + .SetToolVisibility(false); + + scene.FrameBuffer = new(Width, Height, ""); + scene.Prepare(); + + PathTracer pathTracer = new() + { + TotalSpp = NumSamples, + MaxDepth = MaxDepth, + }; + pathTracer.Render(scene); + flip.Add($"PT", scene.FrameBuffer.Image); + renderTimePT = scene.FrameBuffer.RenderTimeMs; + + scene.FrameBuffer = new(Width, Height, ""); + VertexConnectionAndMerging vcm = new() + { + NumIterations = NumSamples, + MaxDepth = MaxDepth, + }; + vcm.Render(scene); + flip.Add($"VCM", scene.FrameBuffer.Image); + renderTimeVCM = scene.FrameBuffer.RenderTimeMs; + } + + SurfacePoint? selected; + + void OnFlipClick(FlipViewer.OnClickEventArgs args) + { + if (args.CtrlKey) + { + selected = scene.RayCast(new(args.X, args.Y)); + } + } + + async Task OnDownloadClick() + { + HtmlReport report = new(); + report.AddMarkdown(""" + # Example experiment + $$ L_\mathrm{o} = \int_\Omega L_\mathrm{i} f_\mathrm{r} |\cos\theta_\mathrm{i}| \, d\omega_\mathrm{i} $$ + """); + report.AddFlipBook(flip); + await SeeSharp.Blazor.Scripts.DownloadAsFile(JS, "report.html", report.ToString()); + } +} \ No newline at end of file diff --git a/Examples/BlenderSync/Pages/Index.razor b/Examples/BlenderSync/Pages/Index.razor new file mode 100644 index 00000000..dcc10eb2 --- /dev/null +++ b/Examples/BlenderSync/Pages/Index.razor @@ -0,0 +1,39 @@ +@page "/" + +@using System.Reflection +@using System.Text.RegularExpressions + + +
+ +
+ + +@code { + /// Enumerates all .razor components in this folder + public IEnumerable<(string Name, string Url)> GetExperimentPages() + { + var routableComponents = Assembly + .GetExecutingAssembly() + .ExportedTypes + .Where(t => t.IsSubclassOf(typeof(ComponentBase))) + .Where(c => c + .GetCustomAttributes(inherit: true) + .OfType() + .Count() > 0); + + foreach (var routableComponent in routableComponents) + { + string name = routableComponent.ToString().Replace("BlenderSync.Pages.", string.Empty); + if (name != "Index") + yield return (name, name); + } + } +} diff --git a/Examples/BlenderSync/Pages/PathViewer.razor b/Examples/BlenderSync/Pages/PathViewer.razor new file mode 100644 index 00000000..ac32a78a --- /dev/null +++ b/Examples/BlenderSync/Pages/PathViewer.razor @@ -0,0 +1,236 @@ +@page "/PathViewer" +@inject SeeSharp.Blender.PathViewerClient Client +@inject SeeSharp.Blender.BlenderCommandSender Commander +@inject SeeSharp.Blender.BlenderEventListener Listener + +@using Markdig.Extensions.SelfPipeline +@using Microsoft.AspNetCore.Routing.Internal +@using SeeSharp.Experiments +@using SeeSharp +@using SeeSharp.Blazor +@using SeeSharp.Common; +@using SeeSharp.Integrators; +@using SeeSharp.Integrators.Bidir; +@using SeeSharp.Blender; +@inject IJSRuntime JS +

Render Scene

+ +
+
+ +
+ +
+ + + + +
    + @foreach (var path in paths) + { +
  • + @path + + +
  • + @if (path == SelectedId) + { + + } + } +
+ +@if (renderedImage != null) +{ + + +
+

Rendered Image

+ +
+} + +@code { + BlenderImporter sceneSelector; + private string renderedImage; + private string hitInfo; + + private List paths = new(); + private string? SelectedId = null; + public PathGraphNode? viewer_root = null; + public Dictionary? viewer_roots = new Dictionary(); + + Scene scene; + Integrator path_tracer = new PathTracer() + { + MaxDepth = 5, + TotalSpp = 1, + EnableDenoiser = false + }; + + Integrator bi_path_tracer = new CameraStoringVCM() + { + NumIterations = 10, + MaxDepth = 10, + EnableDenoiser = false + }; + Integrator integrator = new PathTracer() + { + MaxDepth = 5, + TotalSpp = 1, + EnableDenoiser = false + }; + + void OnModeChanged(ChangeEventArgs e) + { + var SelectedMode = e.Value?.ToString() ?? ""; + + // Update ModeExecutor based on SelectedMode + integrator = SelectedMode switch + { + "PathTracer" => path_tracer, + "Bidir" => bi_path_tracer, + _ => path_tracer + }; + } + PathGraph graph; + RgbColor estimate; + + async Task OnSceneLoaded(SceneFromFile sceneFromFile) + { + await Task.Run(() => scene = sceneFromFile.MakeScene()); + Console.WriteLine("About to load this scene: " + sceneSelector.FindJson(sceneSelector.sceneNameInput.Text)); + + paths.Clear(); + viewer_roots.Clear(); + + await Commander.SendCommandAsync(new + { + command = "import_scene", + scene_name = sceneSelector.FindJson(sceneSelector.sceneNameInput.Text) + }); + } + + async Task Render() + { + scene.FrameBuffer = new(1920, 1080, "../Data/Render.exr"); + scene.Prepare(); + path_tracer.Render(scene); + scene.FrameBuffer.Image.WriteToFile("./Data/Render.png"); + var bytes = File.ReadAllBytes("./Data/Render.png"); + renderedImage = $"data:image/png;base64,{Convert.ToBase64String(bytes)}"; + } + + protected override void OnInitialized() + { + Client._created.OnCreated += obj => + { + paths.Add(obj); + SelectedId = null; + InvokeAsync(StateHasChanged); + }; + Client._deleted.OnDeleted += obj => + { + paths.Remove(obj); + viewer_roots.Remove(obj); + InvokeAsync(StateHasChanged); + }; + Client._selected.OnSelected += obj => + { + SelectedId = obj; + InvokeAsync(StateHasChanged); + }; + } + + async Task OnImageClick(MouseEventArgs e) + { + var pt = await JS.InvokeAsync("getBlenderPixelCoords", + "renderImg", e.ClientX, e.ClientY); + + scene.FrameBuffer = new(1920, 1080, "../Data/Render.exr"); + scene.Prepare(); + (graph, estimate) = integrator.ReplayPixel(scene, new Pixel((int)pt.x, (int)pt.y), 0); + PathGraphNode node = graph.Roots[0]; + viewer_root = graph.Roots[0]; + string jsonGraph = System.Text.Json.JsonSerializer.Serialize(node, new JsonSerializerOptions() + { + IncludeFields = true, + }); + Console.WriteLine(jsonGraph); + string path_id = Guid.NewGuid().ToString(); + viewer_roots.Add(path_id, graph.Roots[0]); + await Commander.SendCommandAsync(new + { + command = "create_path", + id = path_id, + graph = jsonGraph + }); + } + + async Task DeletePath(string id) + { + await Commander.SendCommandAsync(new { command = "delete_path", id }); + } + + async Task SelectPath(string id) + { + await Commander.SendCommandAsync(new { command = "select_path", id }); + } + public class PixelPos { public float x { get; set; } public float y { get; set; } } + + async void OnNodeClicked(PathGraphNode node) + { + var node_list = new List(); + for (var n = node; n != null; n = n.Ancestor) + { + node_list.Add(n.Id); + } + + node_list.Reverse(); + bool is_full_graph = (node == viewer_roots[SelectedId]); + + await Commander.SendCommandAsync(new + { + command = "click_on_node", + path = JsonSerializer.Serialize(node_list, new + JsonSerializerOptions + { + WriteIndented = false + }), + path_id = SelectedId, + is_full_graph + }); + } + async void OnNodeDoubleClicked(PathGraphNode node) + { + var node_list = new List(); + for (var n = node; n != null; n = n.Ancestor) + { + node_list.Add(n.Id); + } + + node_list.Reverse(); + bool is_full_graph = (node == viewer_roots[SelectedId]); + await Commander.SendCommandAsync(new + { + command = "dbclick_on_node", + path = JsonSerializer.Serialize(node_list, new + JsonSerializerOptions + { + WriteIndented = false + }), + path_id = SelectedId, + is_full_graph + }); + } + string GetStyle(string id) + { + return id == SelectedId + ? "background-color: yellow; font-weight: bold;" + : ""; + } +} \ No newline at end of file diff --git a/Examples/BlenderSync/Pages/_Host.cshtml b/Examples/BlenderSync/Pages/_Host.cshtml new file mode 100644 index 00000000..98496a08 --- /dev/null +++ b/Examples/BlenderSync/Pages/_Host.cshtml @@ -0,0 +1,35 @@ +@page "/" +@using Microsoft.AspNetCore.Components.Web +@namespace BlenderSync.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + @Html.Raw(SeeSharp.Blazor.Scripts.AllScripts) + + + + + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + + diff --git a/Examples/BlenderSync/Program.cs b/Examples/BlenderSync/Program.cs new file mode 100644 index 00000000..7cb53f2c --- /dev/null +++ b/Examples/BlenderSync/Program.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +ProgressBar.Silent = true; +SceneRegistry.AddSourceRelativeToScript("../../Data/Scenes"); + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +var eventListener = app.Services.GetRequiredService(); +_ = eventListener.StartAsync(); + +if (!app.Environment.IsDevelopment()) +{ + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +app.Run(); \ No newline at end of file diff --git a/Examples/BlenderSync/Properties/launchSettings.json b/Examples/BlenderSync/Properties/launchSettings.json new file mode 100644 index 00000000..a99c6ef0 --- /dev/null +++ b/Examples/BlenderSync/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "iisSettings": { + "iisExpress": { + "applicationUrl": "http://localhost:18831", + "sslPort": 44326 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7055;http://localhost:5229", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Examples/BlenderSync/_Imports.razor b/Examples/BlenderSync/_Imports.razor new file mode 100644 index 00000000..7d8a1710 --- /dev/null +++ b/Examples/BlenderSync/_Imports.razor @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using BlenderSync + +@using SeeSharp.Blazor diff --git a/Examples/BlenderSync/appsettings.Development.json b/Examples/BlenderSync/appsettings.Development.json new file mode 100644 index 00000000..770d3e93 --- /dev/null +++ b/Examples/BlenderSync/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Examples/BlenderSync/appsettings.json b/Examples/BlenderSync/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Examples/BlenderSync/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Examples/BlenderSync/wwwroot/css/site.css b/Examples/BlenderSync/wwwroot/css/site.css new file mode 100644 index 00000000..ddc98cca --- /dev/null +++ b/Examples/BlenderSync/wwwroot/css/site.css @@ -0,0 +1,86 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 3.5rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +html { + font-family: system-ui; +} + +button { + background-color: #a4e1f2; + border-style: none; + /* border-width: 2px; + border-color: #245e6f; */ + color: black; + font-size: medium; + padding-left: 8px; + padding-right: 8px; + padding-bottom: 4px; + padding-top: 4px; +} + button:hover { + background-color: #c9eff4; + cursor: pointer; + } + button:disabled { + background-color: #e5f1f5; + color: #96b4bd; + border-color: #96b4bd; + } + +.experiment-settings { + display: flex; + gap: 0.25em; + flex-direction: column; + float: left; + margin-right: 1em; +} + +.experiment-results { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: flex-start; +} + +table { + border-collapse: collapse; +} +td, th { + border: none; + padding: 4px; +} +tr:hover { background-color: #e7f2f1; } +th { + padding-top: 6px; + padding-bottom: 6px; + text-align: left; + background-color: #4a96af; + color: white; + font-size: smaller; +} \ No newline at end of file diff --git a/Examples/BlenderSync/wwwroot/raycast.js b/Examples/BlenderSync/wwwroot/raycast.js new file mode 100644 index 00000000..2bd8ffc0 --- /dev/null +++ b/Examples/BlenderSync/wwwroot/raycast.js @@ -0,0 +1,23 @@ +window.getBlenderPixelCoords = function (imgId, clickX, clickY) { + const img = document.getElementById(imgId); + if (!img) return null; + + const rect = img.getBoundingClientRect(); + + // Pixel inside displayed image + const x_ui = clickX - rect.left; + const y_ui = clickY - rect.top; + + // Displayed size + const Wu = rect.width; + const Hu = rect.height; + + // Blender render resolution + const Wb = img.naturalWidth; + const Hb = img.naturalHeight; + + return { + x: x_ui / Wu * Wb, + y: y_ui / Hu * Hb + }; +}; \ No newline at end of file diff --git a/SeeSharp.Examples/MisCompensation.dib b/Examples/MisCompensation.dib similarity index 100% rename from SeeSharp.Examples/MisCompensation.dib rename to Examples/MisCompensation.dib diff --git a/SeeSharp.Examples/SphericalSampling.dib b/Examples/SphericalSampling.dib similarity index 100% rename from SeeSharp.Examples/SphericalSampling.dib rename to Examples/SphericalSampling.dib diff --git a/SeeSharp.Blazor/GraphBrowser.razor b/SeeSharp.Blazor/GraphBrowser.razor new file mode 100644 index 00000000..fd9bada3 --- /dev/null +++ b/SeeSharp.Blazor/GraphBrowser.razor @@ -0,0 +1,67 @@ +@using SeeSharp.Integrators.Util + + +@namespace SeeSharp.Blazor +@if (Node != null) +{ +
+ + @if (HasChildren) + { + @(expanded ? "▼" : "▶") + } + else + { + + } + + + + @Node.GetType().Name + +
+ + @if (expanded && HasChildren) + { +
+ @foreach (var child in Node.Successors) + { + + } +
+ } + +} + +@code { + [Parameter] public PathGraphNode Node { get; set; } = default!; + [Parameter] public Action OnClick { get; set; } = default!; + [Parameter] public Action OnDoubleClick { get; set; } = default!; + [Parameter] public bool ForceExpand { get; set; } = false; + bool expanded = false; + bool HasChildren => Node.Successors?.Count > 0; + void Toggle() => expanded = !expanded; + + bool AutoExpand => + Node is ConnectionNode || Node is MergeNode || ForceExpand; + + protected override void OnParametersSet() + { + if (ForceExpand) + { + expanded = true; + } + } + void Click() + { + OnClick?.Invoke(Node); + } + + void DoubleClick() + { + OnDoubleClick?.Invoke(Node); + } +} \ No newline at end of file diff --git a/SeeSharp.Blazor/GraphBrowser.razor.css b/SeeSharp.Blazor/GraphBrowser.razor.css new file mode 100644 index 00000000..6b647257 --- /dev/null +++ b/SeeSharp.Blazor/GraphBrowser.razor.css @@ -0,0 +1,26 @@ +.tree-node { + display: flex; + align-items: center; + font-family: monospace; +} + +.toggle { + width: 20px; + cursor: pointer; + user-select: none; +} + +.label { + cursor: pointer; + padding: 2px 4px; +} + +.label:hover { + background-color: #2d2d2d; +} + +.children { + margin-left: 20px; + border-left: 1px dotted #555; + padding-left: 6px; +} \ No newline at end of file diff --git a/SeeSharp.Blender/Addons/CursorTracker.cs b/SeeSharp.Blender/Addons/CursorTracker.cs new file mode 100644 index 00000000..9aee36ee --- /dev/null +++ b/SeeSharp.Blender/Addons/CursorTracker.cs @@ -0,0 +1,13 @@ +namespace SeeSharp.Blender +{ + public class CursorTrackerClient + { + public readonly CursorTrackedHandler _cursor_tracked; + public CursorTrackerClient(IEnumerable handlers) + { + _cursor_tracked = handlers + .OfType() + .Single(); // or First() + } + } +} diff --git a/SeeSharp.Blender/Addons/PathViewer.cs b/SeeSharp.Blender/Addons/PathViewer.cs new file mode 100644 index 00000000..75ca4397 --- /dev/null +++ b/SeeSharp.Blender/Addons/PathViewer.cs @@ -0,0 +1,22 @@ +namespace SeeSharp.Blender +{ + public class PathViewerClient + { + public readonly CreatedHandler _created; + public readonly DeletedHandler _deleted; + public readonly SelectedHandler _selected; + + public PathViewerClient(IEnumerable handlers) + { + _created = handlers + .OfType() + .Single(); // or First() + _deleted = handlers + .OfType() + .Single(); + _selected = handlers + .OfType() + .Single(); + } + } +} diff --git a/SeeSharp.Blender/BlenderCommandSender.cs b/SeeSharp.Blender/BlenderCommandSender.cs new file mode 100644 index 00000000..b432ce06 --- /dev/null +++ b/SeeSharp.Blender/BlenderCommandSender.cs @@ -0,0 +1,129 @@ +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +namespace SeeSharp.Blender +{ + public class BlenderCommandSender + { + private TcpClient _client; + private NetworkStream _stream; + private bool _connected = false; + + public async Task TryConnectAsync() + { + try + { + _client = new TcpClient(); + await _client.ConnectAsync("127.0.0.1", 5051); + _stream = _client.GetStream(); + _connected = true; + Console.WriteLine("✔ Connected to Blender cursor port"); + return true; + } + catch (Exception ex) + { + Console.WriteLine("❌ Blender not listening (connection failed): " + ex.Message); + _connected = false; + return false; + } + } + + public async Task SendCursorAsync(float x, float y, float z) + { + // Ensure connection exists + if (true) + { + bool ok = await TryConnectAsync(); + if (!ok) + { + // Do NOT crash — just gracefully fail + Console.WriteLine("⚠ Could not send cursor update, Blender not running"); + return; + } + } + + try + { + var data = new + { + cursor_position = new[] { x, y, z } + }; + + string json = JsonSerializer.Serialize(data) + "\n"; + byte[] bytes = Encoding.UTF8.GetBytes(json); + + await _stream.WriteAsync(bytes, 0, bytes.Length); + await _stream.FlushAsync(); + + Console.WriteLine("➡ Sent cursor update to Blender: " + json); + } + catch (Exception ex) + { + Console.WriteLine("❌ Error sending cursor data: " + ex.Message); + + // Force cleanup of dead connection + try { _stream?.Close(); } catch { } + try { _client?.Close(); } catch { } + + _stream = null; + _client = null; + _connected = false; + } + } + + public async Task SendCommandAsync(object cmd) + { + if (!_connected || !IsSocketConnected()) + { + _connected = false; + + Console.WriteLine("🔌 Lost connection — reconnecting…"); + bool ok = await TryConnectAsync(); + + if (!ok) + { + Console.WriteLine("⚠ Failed to reconnect"); + return; + } + } + try + { + string json = JsonSerializer.Serialize(cmd) + "\n"; + byte[] bytes = Encoding.UTF8.GetBytes(json); + + await _stream.WriteAsync(bytes, 0, bytes.Length); + await _stream.FlushAsync(); + + Console.WriteLine("➡ Sent command: " + json); + } + catch (Exception ex) + { + Console.WriteLine("❌ Error sending command: " + ex.Message); + _connected = false; + } + } + private bool IsSocketConnected() + { + try + { + if (_client == null || !_client.Connected) + return false; + + var s = _client.Client; + + // Check if the socket has been closed + bool readReady = s.Poll(0, SelectMode.SelectRead); + bool noBytes = (s.Available == 0); + + if (readReady && noBytes) + return false; + + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/BlenderEventDispatcher.cs b/SeeSharp.Blender/BlenderEventDispatcher.cs new file mode 100644 index 00000000..dfb346b3 --- /dev/null +++ b/SeeSharp.Blender/BlenderEventDispatcher.cs @@ -0,0 +1,46 @@ +using System.Text.Json; + +namespace SeeSharp.Blender +{ + public interface IBlenderEventHandler +{ + string EventType { get; } + void Handle(JsonElement root); +} + +public class BlenderEventDispatcher +{ + private readonly Dictionary _handlers; + + public BlenderEventDispatcher(IEnumerable handlers) + { + _handlers = handlers.ToDictionary(h => h.EventType); + } + + public void Register(IBlenderEventHandler handler) + { + _handlers.Add(handler.EventType, handler); + } + + public void Unregister(IBlenderEventHandler handler) + { + _handlers.Remove(handler.EventType); + } + + public void Dispatch(JsonElement root) + { + if (!root.TryGetProperty("event", out var evtProp)) + return; + + var evt = evtProp.GetString(); + if (evt == null) + return; + + if (_handlers.TryGetValue(evt, out var handler)) + handler.Handle(root); + else + Console.WriteLine($"⚠️ Unknown Blender event: {evt}"); + } +} + +} \ No newline at end of file diff --git a/SeeSharp.Blender/BlenderEventListener.cs b/SeeSharp.Blender/BlenderEventListener.cs new file mode 100644 index 00000000..5f9ec2ec --- /dev/null +++ b/SeeSharp.Blender/BlenderEventListener.cs @@ -0,0 +1,66 @@ +using System.Net.Sockets; +using System.Net; +using System.Text.Json; +namespace SeeSharp.Blender +{ + public class BlenderEventListener +{ + private TcpListener? _listener; + private BlenderEventDispatcher _dispatcher; + + public BlenderEventListener(BlenderEventDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public void RegisterDispatcher(BlenderEventDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public async Task StartAsync() + { + _listener = new TcpListener(IPAddress.Loopback, 5052); + _listener.Start(); + Console.WriteLine("📡 Listening for Blender events on 5052..."); + + // while (true) + // { + // var client = await _listener.AcceptTcpClientAsync(); + // _ = HandleClientAsync(client); + // } + _ = Task.Run(async () => + { + while (true) + { + var client = await _listener.AcceptTcpClientAsync(); + Console.WriteLine("🔌 Blender connected to Blazor Event Listener"); + + _ = HandleClientAsync(client); + } + }); + } + + private async Task HandleClientAsync(TcpClient client) + { + using var reader = new StreamReader(client.GetStream()); + + while (true) + { + var line = await reader.ReadLineAsync(); + if (line == null) break; + Console.WriteLine("📨 Received event: " + line); + try + { + using var doc = JsonDocument.Parse(line); + _dispatcher.Dispatch(doc.RootElement); + } + catch (Exception ex) + { + Console.WriteLine("❌ Parse error: " + ex.Message); + } + } + } +} + +} \ No newline at end of file diff --git a/SeeSharp.Blender/Handlers/CreatedHandler.cs b/SeeSharp.Blender/Handlers/CreatedHandler.cs new file mode 100644 index 00000000..261f4ec7 --- /dev/null +++ b/SeeSharp.Blender/Handlers/CreatedHandler.cs @@ -0,0 +1,13 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class CreatedHandler : IBlenderEventHandler +{ + public string EventType => "created"; + + public event Action? OnCreated; + + public void Handle(JsonElement root) + { + OnCreated?.Invoke(root.GetProperty("id").GetString()); + } +} diff --git a/SeeSharp.Blender/Handlers/CursorTrackedHandler.cs b/SeeSharp.Blender/Handlers/CursorTrackedHandler.cs new file mode 100644 index 00000000..78824a5f --- /dev/null +++ b/SeeSharp.Blender/Handlers/CursorTrackedHandler.cs @@ -0,0 +1,38 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class BlenderCursorData + { + // [JsonPropertyName("object")] + public string? Object { get; set; } + // [JsonPropertyName("cursor_position")] + public float[] Cursor_Position { get; set; } = Array.Empty(); + // [JsonPropertyName("hit_position")] + public float[]? Hit_Position { get; set; } + // [JsonPropertyName("face_index")] + public int? Face_Index { get; set; } + // [JsonPropertyName("normal")] + public float[]? Normal { get; set; } + } + +public class CursorTrackedHandler : IBlenderEventHandler +{ + public string EventType => "cursor_tracked"; + public event Action? OnCursorTracked; + + public void Handle(JsonElement root) + { + OnCursorTracked?.Invoke(new BlenderCursorData + { + Object = root.TryGetProperty("object", out var obj) ? + obj.GetString() : null, + Cursor_Position = + JsonSerializer.Deserialize(root.GetProperty("cursor_position")), + Hit_Position = root.TryGetProperty("hit_position", out var pos) ? + JsonSerializer.Deserialize(pos.GetRawText()) : null, + Face_Index = root.TryGetProperty("face_index", out var face) ? + face.GetInt32() : null, + Normal = root.TryGetProperty("normal", out var norm) ? + JsonSerializer.Deserialize(norm.GetRawText()) : null, + }); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Handlers/DeletedHandler.cs b/SeeSharp.Blender/Handlers/DeletedHandler.cs new file mode 100644 index 00000000..0e419a72 --- /dev/null +++ b/SeeSharp.Blender/Handlers/DeletedHandler.cs @@ -0,0 +1,12 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class DeletedHandler : IBlenderEventHandler +{ + public string EventType => "deleted"; + public event Action? OnDeleted; + + public void Handle(JsonElement root) + { + OnDeleted?.Invoke(root.GetProperty("id").GetString()); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/Handlers/SelectedHandler.cs b/SeeSharp.Blender/Handlers/SelectedHandler.cs new file mode 100644 index 00000000..06245874 --- /dev/null +++ b/SeeSharp.Blender/Handlers/SelectedHandler.cs @@ -0,0 +1,12 @@ +using SeeSharp.Blender; +using System.Text.Json; +public class SelectedHandler : IBlenderEventHandler +{ + public string EventType => "selected"; + public event Action? OnSelected; + + public void Handle(JsonElement root) + { + OnSelected?.Invoke(root.GetProperty("id").GetString()); + } +} \ No newline at end of file diff --git a/SeeSharp.Blender/SeeSharp.Blender.csproj b/SeeSharp.Blender/SeeSharp.Blender.csproj new file mode 100644 index 00000000..f773e381 --- /dev/null +++ b/SeeSharp.Blender/SeeSharp.Blender.csproj @@ -0,0 +1,12 @@ + + + + net9.0 + enable + enable + + + + + + \ No newline at end of file diff --git a/SeeSharp.Examples/MakeFigure.py b/SeeSharp.Examples/MakeFigure.py deleted file mode 100644 index c8d45cd5..00000000 --- a/SeeSharp.Examples/MakeFigure.py +++ /dev/null @@ -1,48 +0,0 @@ -import figuregen -from figuregen.util.image import Cropbox -from figuregen.util.templates import FullSizeWithCrops -import simpleimageio as sio -import sys, os - -def make_figure(dirname, method_names): - """ - Creates a simple overview figure using the FullSizeWithCrops template. - Assumes the given directory contains a reference image and subdirectories for each method: - - - Reference.exr - - method_names[0].exr - - method_names[1].exr - """ - names = ["Reference"] - names.extend(method_names) - return FullSizeWithCrops( - reference_image=sio.read(os.path.join(dirname, "Reference.exr")), - method_images=[ - sio.read(os.path.join(dirname, f"{name}.exr")) - for name in method_names - ], - method_names=names, - crops=[ - Cropbox(top=345, left=25, width=64, height=48, scale=4), - Cropbox(top=155, left=200, width=64, height=48, scale=4), - ] - ).figure - -if __name__ == "__main__": - result_dir = sys.argv[1] - - method_names = [] - for i in range(2, len(sys.argv)): - method_names.append(sys.argv[i]) - - # Find all scenes by enumerating the result directory - rows = [] - for path in os.listdir(result_dir): - if not os.path.isdir(os.path.join(result_dir, path)): - continue - try: - rows.extend(make_figure(os.path.join(result_dir, path), method_names)) - except: - print(f"skipping scene with invalid data: {path}") - - figuregen.figure(rows, 18, os.path.join(result_dir, "Overview.pdf")) \ No newline at end of file diff --git a/SeeSharp.Examples/PathVsVcm.cs b/SeeSharp.Examples/PathVsVcm.cs deleted file mode 100644 index d33ec990..00000000 --- a/SeeSharp.Examples/PathVsVcm.cs +++ /dev/null @@ -1,16 +0,0 @@ -using SeeSharp.Experiments; -using SeeSharp.Integrators; -using SeeSharp.Integrators.Bidir; -using System.Collections.Generic; - -namespace SeeSharp.Examples; - -/// -/// Renders a scene with a path tracer and with VCM. -/// -class PathVsVcm : Experiment { - public override List MakeMethods() => [ - new("PathTracer", new PathTracer() { TotalSpp = 4 }), - new("Vcm", new VertexConnectionAndMerging() { NumIterations = 2 }) - ]; -} \ No newline at end of file diff --git a/SeeSharp.Examples/Program.cs b/SeeSharp.Examples/Program.cs deleted file mode 100644 index 928e49db..00000000 --- a/SeeSharp.Examples/Program.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Diagnostics; -using SeeSharp.Experiments; -using SeeSharp.Images; -using SeeSharp.Examples; - -// Register the directory as a scene file provider. -// Asides from the geometry, it is also used as a reference image cache. -SceneRegistry.AddSource("Data/Scenes"); - -// Configure a benchmark to compare path tracing and VCM on the CornellBox -// at 512x512 resolution. Display images in tev during rendering (localhost, default port) -Benchmark benchmark = new(new PathVsVcm(), [ - SceneRegistry.LoadScene("CornellBox", maxDepth: 5), - // SceneRegistry.LoadScene("CornellBox", maxDepth: 2).WithName("CornellBoxDirectIllum") -], "Results/PathVsVcm", 512, 512, FrameBuffer.Flags.SendToTev); - -// Render the images -benchmark.Run(); - -// Optional, but usually a good idea: assemble the rendering results in an overview -// figure using a Python script. -Process.Start("python", "./SeeSharp.Examples/MakeFigure.py Results/PathVsVcm PathTracer Vcm") - .WaitForExit(); - -// For our README file, we further convert the pdf to png with ImageMagick -Process.Start("magick", "-density 300 ./Results/PathVsVcm/Overview.pdf ExampleFigure.png") - .WaitForExit(); diff --git a/SeeSharp.Examples/SeeSharp.Examples.csproj b/SeeSharp.Examples/SeeSharp.Examples.csproj deleted file mode 100644 index 1bebeb4c..00000000 --- a/SeeSharp.Examples/SeeSharp.Examples.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - Exe - net9.0 - false - - - diff --git a/SeeSharp.sln b/SeeSharp.sln index c0161e52..b1687443 100644 --- a/SeeSharp.sln +++ b/SeeSharp.sln @@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.IntegrationTests", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Benchmark", "SeeSharp.Benchmark\SeeSharp.Benchmark.csproj", "{957383F5-B5A0-4CA5-A283-0F2154FFC96A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Examples", "SeeSharp.Examples\SeeSharp.Examples.csproj", "{5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.PreviewRender", "SeeSharp.PreviewRender\SeeSharp.PreviewRender.csproj", "{93EBE289-351C-4F29-95CA-B559F3675031}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.ToMitsuba", "SeeSharp.ToMitsuba\SeeSharp.ToMitsuba.csproj", "{83FE3967-6070-4BF3-8861-6410B877CB45}" @@ -25,6 +23,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Blazor", "SeeSharp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialTest", "MaterialTest\MaterialTest.csproj", "{5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeeSharp.Blender", "SeeSharp.Blender\SeeSharp.Blender.csproj", "{BB86935A-DF0E-4B98-9823-41A15F85BD7D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlenderSync", "Examples\BlenderSync\BlenderSync.csproj", "{B7B4BA23-171F-49F2-B711-654C3D8F2A18}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,9 +38,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B8C0540-28DA-4071-A3B1-03391D07630F}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -98,18 +99,6 @@ Global {957383F5-B5A0-4CA5-A283-0F2154FFC96A}.Release|x64.Build.0 = Release|Any CPU {957383F5-B5A0-4CA5-A283-0F2154FFC96A}.Release|x86.ActiveCfg = Release|Any CPU {957383F5-B5A0-4CA5-A283-0F2154FFC96A}.Release|x86.Build.0 = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x64.ActiveCfg = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x64.Build.0 = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x86.ActiveCfg = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Debug|x86.Build.0 = Debug|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|Any CPU.Build.0 = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x64.ActiveCfg = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x64.Build.0 = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x86.ActiveCfg = Release|Any CPU - {5FB3E5AD-2DB5-4A84-9E03-268AF95C5C6D}.Release|x86.Build.0 = Release|Any CPU {93EBE289-351C-4F29-95CA-B559F3675031}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {93EBE289-351C-4F29-95CA-B559F3675031}.Debug|Any CPU.Build.0 = Debug|Any CPU {93EBE289-351C-4F29-95CA-B559F3675031}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -170,5 +159,35 @@ Global {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x64.Build.0 = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.ActiveCfg = Release|Any CPU {5440A486-D6C5-47C8-8B53-C09F1EFE0A7F}.Release|x86.Build.0 = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x64.ActiveCfg = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x64.Build.0 = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x86.ActiveCfg = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Debug|x86.Build.0 = Debug|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|Any CPU.Build.0 = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x64.ActiveCfg = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x64.Build.0 = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x86.ActiveCfg = Release|Any CPU + {BB86935A-DF0E-4B98-9823-41A15F85BD7D}.Release|x86.Build.0 = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x64.Build.0 = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Debug|x86.Build.0 = Debug|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|Any CPU.Build.0 = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x64.ActiveCfg = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x64.Build.0 = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x86.ActiveCfg = Release|Any CPU + {B7B4BA23-171F-49F2-B711-654C3D8F2A18}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B7B4BA23-171F-49F2-B711-654C3D8F2A18} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} EndGlobalSection EndGlobal diff --git a/SeeSharp/Integrators/Util/PathGraph.cs b/SeeSharp/Integrators/Util/PathGraph.cs index dce389af..824b4d10 100644 --- a/SeeSharp/Integrators/Util/PathGraph.cs +++ b/SeeSharp/Integrators/Util/PathGraph.cs @@ -1,150 +1,6 @@ using System.Linq; namespace SeeSharp.Integrators.Util; - -public class PathGraphNode(Vector3 pos, PathGraphNode ancestor = null) { - public Vector3 Position = pos; - public PathGraphNode Ancestor = ancestor; - public List Successors = []; - - public virtual bool IsBackground => false; - - public PathGraphNode AddSuccessor(PathGraphNode vertex) { - Successors.Add(vertex); - vertex.Ancestor = this; - return vertex; - } - - public virtual RgbColor ComputeVisualizerColor() { - return RgbColor.Black; - } - - public PathGraphNode Clone() { - var result = MemberwiseClone() as PathGraphNode; - result.Successors = []; - foreach (var s in Successors) { - var sClone = s.Clone(); - sClone.Ancestor = result; - result.Successors.Add(sClone); - } - return result; - } -} - -public interface IContribNode { - public RgbColor Contrib { get; } - public float MISWeight { get; } -} - -public class NextEventNode : PathGraphNode, IContribNode { - public NextEventNode(Vector3 direction, PathGraphNode ancestor, RgbColor emission, float pdf, - RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) - : base(ancestor.Position + direction, ancestor) { - Emission = emission; - Pdf = pdf; - BsdfTimesCosine = bsdfCos; - MISWeight = misWeight; - PrefixWeight = prefixWeight; - } - - public NextEventNode(SurfacePoint point, RgbColor emission, float pdf, RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) - : base(point.Position) { - Emission = emission; - Pdf = pdf; - BsdfTimesCosine = bsdfCos; - MISWeight = misWeight; - Point = point; - PrefixWeight = prefixWeight; - } - - public readonly RgbColor Emission; - public readonly float Pdf; - public readonly RgbColor BsdfTimesCosine; - public readonly float MISWeight; - public readonly SurfacePoint? Point; - public readonly RgbColor PrefixWeight; - - public override bool IsBackground => !Point.HasValue; - - public RgbColor Contrib => PrefixWeight * Emission / Pdf * BsdfTimesCosine; - - float IContribNode.MISWeight => MISWeight; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); -} - -public class BSDFSampleNode : PathGraphNode { - public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb) : base(point.Position) { - ScatterWeight = scatterWeight; - SurvivalProbability = survivalProb; - Point = point; - } - - public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb, RgbColor emission, float misWeight) : base(point.Position) { - ScatterWeight = scatterWeight; - SurvivalProbability = survivalProb; - Emission = emission; - MISWeight = misWeight; - Point = point; - } - - public readonly RgbColor ScatterWeight; - public readonly float SurvivalProbability; - public readonly RgbColor Emission; - public readonly float MISWeight; - public readonly SurfacePoint Point; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(41, 107, 177); -} - -public class LightPathNode(PathVertex lightVertex) : PathGraphNode(lightVertex.Point.Position) { - public readonly PathVertex LightVertex = lightVertex; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(228, 135, 17); -} - -public class ConnectionNode : PathGraphNode, IContribNode { - public ConnectionNode(PathVertex lightVertex, float misWeight, RgbColor contrib) - : base(lightVertex.Point.Position) { - Contrib = contrib; - MISWeight = misWeight; - LightVertex = lightVertex; - } - - public RgbColor Contrib { get; init; } - public float MISWeight { get; init; } - public readonly PathVertex LightVertex; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(167, 214, 170); -} - -public class MergeNode : PathGraphNode, IContribNode { - public MergeNode(PathVertex lightVertex, float misWeight, RgbColor contrib) - : base(lightVertex.Point.Position) { - Contrib = contrib; - MISWeight = misWeight; - LightVertex = lightVertex; - } - - public RgbColor Contrib { get; init; } - public float MISWeight { get; init; } - public readonly PathVertex LightVertex; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(218, 152, 204); -} - -public class BackgroundNode : PathGraphNode { - public BackgroundNode(Vector3 direction, PathGraphNode ancestor, RgbColor contrib, float misWeight) : base(ancestor.Position + direction) { - Emission = contrib; - MISWeight = misWeight; - } - public readonly RgbColor Emission; - public readonly float MISWeight; - public override bool IsBackground => true; - - public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); -} - public class PathGraph { public List Roots = []; diff --git a/SeeSharp/Integrators/Util/PathGraph/BSDFSampleNode.cs b/SeeSharp/Integrators/Util/PathGraph/BSDFSampleNode.cs new file mode 100644 index 00000000..f4e219c9 --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/BSDFSampleNode.cs @@ -0,0 +1,25 @@ +namespace SeeSharp.Integrators.Util; + +public class BSDFSampleNode : PathGraphNode { + public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb) : base(point.Position) { + ScatterWeight = scatterWeight; + SurvivalProbability = survivalProb; + Point = point; + } + + public BSDFSampleNode(SurfacePoint point, RgbColor scatterWeight, float survivalProb, RgbColor emission, float misWeight) : base(point.Position) { + ScatterWeight = scatterWeight; + SurvivalProbability = survivalProb; + Emission = emission; + MISWeight = misWeight; + Point = point; + } + + public readonly RgbColor ScatterWeight; + public readonly float SurvivalProbability; + public readonly RgbColor Emission; + public readonly float MISWeight; + public SurfacePoint Point { get; } + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(41, 107, 177); +} diff --git a/SeeSharp/Integrators/Util/PathGraph/BackgroundNode.cs b/SeeSharp/Integrators/Util/PathGraph/BackgroundNode.cs new file mode 100644 index 00000000..a8a1bdfe --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/BackgroundNode.cs @@ -0,0 +1,13 @@ +namespace SeeSharp.Integrators.Util; + +public class BackgroundNode : PathGraphNode { + public BackgroundNode(Vector3 direction, PathGraphNode ancestor, RgbColor contrib, float misWeight) : base(ancestor.Position + direction) { + Emission = contrib; + MISWeight = misWeight; + } + public readonly RgbColor Emission; + public readonly float MISWeight; + public override bool IsBackground => true; + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/ConnectionNode.cs b/SeeSharp/Integrators/Util/PathGraph/ConnectionNode.cs new file mode 100644 index 00000000..5e1de10a --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/ConnectionNode.cs @@ -0,0 +1,16 @@ +namespace SeeSharp.Integrators.Util; + +public class ConnectionNode : PathGraphNode, IContribNode { + public ConnectionNode(PathVertex lightVertex, float misWeight, RgbColor contrib) + : base(lightVertex.Point.Position) { + Contrib = contrib; + MISWeight = misWeight; + LightVertex = lightVertex; + } + + public RgbColor Contrib { get; init; } + public float MISWeight { get; init; } + public PathVertex LightVertex { get; } + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(167, 214, 170); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/LightPathNode.cs b/SeeSharp/Integrators/Util/PathGraph/LightPathNode.cs new file mode 100644 index 00000000..8cb19022 --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/LightPathNode.cs @@ -0,0 +1,7 @@ +namespace SeeSharp.Integrators.Util; + +public class LightPathNode(PathVertex lightVertex) : PathGraphNode(lightVertex.Point.Position) { + public PathVertex LightVertex { get; } = lightVertex; + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(228, 135, 17); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/MergeNode.cs b/SeeSharp/Integrators/Util/PathGraph/MergeNode.cs new file mode 100644 index 00000000..cf12e04a --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/MergeNode.cs @@ -0,0 +1,16 @@ +namespace SeeSharp.Integrators.Util; + +public class MergeNode : PathGraphNode, IContribNode { + public MergeNode(PathVertex lightVertex, float misWeight, RgbColor contrib) + : base(lightVertex.Point.Position) { + Contrib = contrib; + MISWeight = misWeight; + LightVertex = lightVertex; + } + + public RgbColor Contrib { get; init; } + public float MISWeight { get; init; } + public PathVertex LightVertex { get; } + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(218, 152, 204); +} \ No newline at end of file diff --git a/SeeSharp/Integrators/Util/PathGraph/NextEventNode.cs b/SeeSharp/Integrators/Util/PathGraph/NextEventNode.cs new file mode 100644 index 00000000..aa7dace3 --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/NextEventNode.cs @@ -0,0 +1,37 @@ +namespace SeeSharp.Integrators.Util; +public class NextEventNode : PathGraphNode, IContribNode { + public NextEventNode(Vector3 direction, PathGraphNode ancestor, RgbColor emission, float pdf, + RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) + : base(ancestor.Position + direction, ancestor) { + Emission = emission; + Pdf = pdf; + BsdfTimesCosine = bsdfCos; + MISWeight = misWeight; + PrefixWeight = prefixWeight; + } + + public NextEventNode(SurfacePoint point, RgbColor emission, float pdf, RgbColor bsdfCos, float misWeight, RgbColor prefixWeight) + : base(point.Position) { + Emission = emission; + Pdf = pdf; + BsdfTimesCosine = bsdfCos; + MISWeight = misWeight; + Point = point; + PrefixWeight = prefixWeight; + } + + public readonly RgbColor Emission; + public readonly float Pdf; + public readonly RgbColor BsdfTimesCosine; + public readonly float MISWeight; + public readonly SurfacePoint? Point; + public readonly RgbColor PrefixWeight; + + public override bool IsBackground => !Point.HasValue; + + public RgbColor Contrib => PrefixWeight * Emission / Pdf * BsdfTimesCosine; + + float IContribNode.MISWeight => MISWeight; + + public override RgbColor ComputeVisualizerColor() => RgbColor.SrgbToLinear(170, 231, 232); +} diff --git a/SeeSharp/Integrators/Util/PathGraph/PathGraphNode.cs b/SeeSharp/Integrators/Util/PathGraph/PathGraphNode.cs new file mode 100644 index 00000000..53ccfb2f --- /dev/null +++ b/SeeSharp/Integrators/Util/PathGraph/PathGraphNode.cs @@ -0,0 +1,47 @@ +namespace SeeSharp.Integrators.Util; + +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(PathGraphNode), "PathGraphNode")] +[JsonDerivedType(typeof(NextEventNode), "NextEventNode")] +[JsonDerivedType(typeof(BSDFSampleNode), "BSDFSampleNode")] +[JsonDerivedType(typeof(LightPathNode), "LightPathNode")] +[JsonDerivedType(typeof(ConnectionNode), "ConnectionNode")] +[JsonDerivedType(typeof(MergeNode), "MergeNode")] +[JsonDerivedType(typeof(BackgroundNode), "BackgroundNode")] +public class PathGraphNode(Vector3 pos, PathGraphNode ancestor = null) { + public string Id { get; init; } = Guid.NewGuid().ToString("N")[..30]; + public Vector3 Position { get; set; } = pos; + [JsonIgnore] public PathGraphNode Ancestor = ancestor; + + [JsonPropertyName("ancestorId")] + public string? AncestorId => Ancestor?.Id; + public List Successors = []; + + public virtual bool IsBackground => false; + + public PathGraphNode AddSuccessor(PathGraphNode vertex) { + Successors.Add(vertex); + vertex.Ancestor = this; + return vertex; + } + + public virtual RgbColor ComputeVisualizerColor() { + return RgbColor.Black; + } + + public PathGraphNode Clone() { + var result = MemberwiseClone() as PathGraphNode; + result.Successors = []; + foreach (var s in Successors) { + var sClone = s.Clone(); + sClone.Ancestor = result; + result.Successors.Add(sClone); + } + return result; + } +} + +public interface IContribNode { + public RgbColor Contrib { get; } + public float MISWeight { get; } +} \ No newline at end of file diff --git a/justfile b/justfile index eebeff0b..75875147 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ _blender_binaries: # Builds the Blender add-on .zip [working-directory: "./BlenderExtension/see_blender/"] blender: _build_dotnet _blender_binaries - blender --command extension build --output-dir .. + blender --factory-startup --command extension build --output-dir .. @echo "" @echo "Blender plugin built. Open Blender and go to 'Edit - Preferences - Addons - Install from Disk' (dropdown menu in the top-right corner)" @echo "Browse to the 'BlenderExtension/see_sharp_renderer-VERSION.zip' file in this directory and install it."