""" Rendering & Lighting Tools Fine-grained control over scene lighting, materials, background, fog, post-processing effects, and camera effects. """ from typing import Dict, Any, Optional from backend.storage import storage def add_light( scene_id: str, light_type: str, name: str, color: str = "#ffffff", intensity: float = 1.0, position: Optional[Dict[str, float]] = None, target: Optional[Dict[str, float]] = None, cast_shadow: bool = False, spot_angle: Optional[float] = None ) -> Dict[str, Any]: """ Implementation: Add a new light source to the scene Args: scene_id: ID of the scene light_type: "ambient" | "directional" | "point" | "spot" name: Light identifier (e.g., "Torch1", "MainLight") color: Hex color (default: "#ffffff") intensity: Brightness 0.0-2.0 (default: 1.0) position: Position for directional/point/spot lights target: Target position for directional/spot lights cast_shadow: Enable shadows (default: False) spot_angle: Cone angle in degrees (spot lights only) Returns: Dictionary with light details and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") # Validate light type valid_types = ["ambient", "directional", "point", "spot"] if light_type not in valid_types: raise ValueError(f"Invalid light_type '{light_type}'. Must be one of: {valid_types}") # Check for duplicate name if "lights" not in scene: scene["lights"] = [] for light in scene["lights"]: if light.get("name") == name: raise ValueError(f"Light with name '{name}' already exists. Use update_light() to modify it.") # Create light object light_obj = { "name": name, "light_type": light_type, "color": color, "intensity": intensity, "cast_shadow": cast_shadow } # Add position for non-ambient lights if light_type != "ambient": if position: light_obj["position"] = position else: # Default positions defaults = { "directional": {"x": 50, "y": 50, "z": 50}, "point": {"x": 0, "y": 5, "z": 0}, "spot": {"x": 0, "y": 5, "z": 0} } light_obj["position"] = defaults.get(light_type, {"x": 0, "y": 5, "z": 0}) # Add target for directional/spot lights if light_type in ["directional", "spot"] and target: light_obj["target"] = target # Add spot angle for spot lights if light_type == "spot": light_obj["spot_angle"] = spot_angle if spot_angle else 45.0 scene["lights"].append(light_obj) storage.save(scene) return { "scene_id": scene_id, "message": f"Added {light_type} light '{name}' to scene", "light": light_obj } def remove_light(scene_id: str, light_name: str) -> Dict[str, Any]: """ Implementation: Remove a light from the scene Args: scene_id: ID of the scene light_name: Name of light to remove Returns: Dictionary with confirmation message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "lights" not in scene or not scene["lights"]: raise ValueError(f"Scene has no lights to remove") # Find and remove light original_count = len(scene["lights"]) scene["lights"] = [light for light in scene["lights"] if light.get("name") != light_name] if len(scene["lights"]) == original_count: raise ValueError(f"Light '{light_name}' not found in scene") storage.save(scene) return { "scene_id": scene_id, "message": f"Removed light '{light_name}' from scene", "light_name": light_name } def update_light( scene_id: str, light_name: str, color: Optional[str] = None, intensity: Optional[float] = None, position: Optional[Dict[str, float]] = None, cast_shadow: Optional[bool] = None ) -> Dict[str, Any]: """ Implementation: Update existing light properties Args: scene_id: ID of the scene light_name: Name of light to update color: New color (optional) intensity: New intensity (optional) position: New position (optional) cast_shadow: Enable/disable shadows (optional) Returns: Dictionary with updated light and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "lights" not in scene or not scene["lights"]: raise ValueError(f"Scene has no lights") # Find light light = None for l in scene["lights"]: if l.get("name") == light_name: light = l break if not light: raise ValueError(f"Light '{light_name}' not found in scene") # Update properties updated_props = [] if color is not None: light["color"] = color updated_props.append(f"color={color}") if intensity is not None: light["intensity"] = intensity updated_props.append(f"intensity={intensity}") if position is not None: light["position"] = position updated_props.append(f"position={position}") if cast_shadow is not None: light["cast_shadow"] = cast_shadow updated_props.append(f"shadows={'on' if cast_shadow else 'off'}") storage.save(scene) return { "scene_id": scene_id, "message": f"Updated light '{light_name}': {', '.join(updated_props)}", "light": light } def get_lights(scene_id: str) -> Dict[str, Any]: """ Implementation: Get all lights in the scene Args: scene_id: ID of the scene Returns: Dictionary with list of all lights """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") lights = scene.get("lights", []) return { "scene_id": scene_id, "lights": lights, "count": len(lights) } def update_object_material( scene_id: str, object_id: str, color: Optional[str] = None, metalness: Optional[float] = None, roughness: Optional[float] = None, opacity: Optional[float] = None, emissive: Optional[str] = None, emissive_intensity: Optional[float] = None ) -> Dict[str, Any]: """ Implementation: Update an object's material properties Args: scene_id: ID of the scene object_id: Object to update color: Hex color (optional) metalness: 0.0-1.0 (optional) roughness: 0.0-1.0 (optional) opacity: 0.0-1.0 (optional) emissive: Emissive color for glow (optional) emissive_intensity: Glow strength (optional) Returns: Dictionary with updated material and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "objects" not in scene or not scene["objects"]: raise ValueError(f"Scene has no objects") # Find object obj = None for o in scene["objects"]: if o.get("object_id") == object_id: obj = o break if not obj: raise ValueError(f"Object '{object_id}' not found in scene") # Ensure material exists if "material" not in obj: obj["material"] = {} # Update material properties updated_props = [] if color is not None: obj["material"]["color"] = color updated_props.append(f"color={color}") if metalness is not None: obj["material"]["metalness"] = max(0.0, min(1.0, metalness)) updated_props.append(f"metalness={metalness}") if roughness is not None: obj["material"]["roughness"] = max(0.0, min(1.0, roughness)) updated_props.append(f"roughness={roughness}") if opacity is not None: obj["material"]["opacity"] = max(0.0, min(1.0, opacity)) updated_props.append(f"opacity={opacity}") if emissive is not None: obj["material"]["emissive"] = emissive updated_props.append(f"emissive={emissive}") if emissive_intensity is not None: obj["material"]["emissive_intensity"] = emissive_intensity updated_props.append(f"emissive_intensity={emissive_intensity}") storage.save(scene) return { "scene_id": scene_id, "object_id": object_id, "message": f"Updated material: {', '.join(updated_props)}", "material": obj["material"] } def set_background_color( scene_id: str, color: Optional[str] = None, bg_type: str = "solid", gradient_top: Optional[str] = None, gradient_bottom: Optional[str] = None ) -> Dict[str, Any]: """ Implementation: Set scene background color Args: scene_id: ID of the scene color: Hex color for solid background bg_type: "solid" | "gradient" gradient_top: Top color for gradient gradient_bottom: Bottom color for gradient Returns: Dictionary with background settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "environment" not in scene: scene["environment"] = {} if bg_type == "gradient": if not gradient_top or not gradient_bottom: raise ValueError("gradient_top and gradient_bottom are required for gradient backgrounds") scene["environment"]["background_type"] = "gradient" scene["environment"]["background_gradient_top"] = gradient_top scene["environment"]["background_gradient_bottom"] = gradient_bottom message = f"Set background to gradient: {gradient_top} → {gradient_bottom}" else: if not color: raise ValueError("color is required for solid backgrounds") scene["environment"]["background_type"] = "solid" scene["environment"]["background_color"] = color message = f"Set background to {color}" storage.save(scene) return { "scene_id": scene_id, "message": message, "background": scene["environment"] } def set_fog( scene_id: str, enabled: bool, color: Optional[str] = None, near: Optional[float] = None, far: Optional[float] = None, density: Optional[float] = None ) -> Dict[str, Any]: """ Implementation: Set atmospheric fog Args: scene_id: ID of the scene enabled: Enable/disable fog color: Fog color (default: "#aaaaaa") near: Start distance for linear fog far: End distance for linear fog density: Density for exponential fog Returns: Dictionary with fog settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "environment" not in scene: scene["environment"] = {} if "fog" not in scene["environment"]: scene["environment"]["fog"] = {} fog = scene["environment"]["fog"] fog["enabled"] = enabled if enabled: fog["color"] = color if color else "#aaaaaa" # Determine fog type if density is not None: fog["type"] = "exponential" fog["density"] = density message = f"Enabled exponential fog (density={density}, color={fog['color']})" elif near is not None and far is not None: fog["type"] = "linear" fog["near"] = near fog["far"] = far message = f"Enabled linear fog (near={near}, far={far}, color={fog['color']})" else: # Default linear fog fog["type"] = "linear" fog["near"] = 10 fog["far"] = 50 message = f"Enabled linear fog (near=10, far=50, color={fog['color']})" else: message = "Disabled fog" storage.save(scene) return { "scene_id": scene_id, "message": message, "fog": fog } # ============================================================================= # Post-Processing Tools # ============================================================================= def set_bloom( scene_id: str, enabled: bool, strength: float = 1.0, radius: float = 0.4, threshold: float = 0.8 ) -> Dict[str, Any]: """ Configure bloom (glow) post-processing effect. Bloom creates a glow effect around bright areas of the scene, simulating how cameras capture bright light sources. Args: scene_id: ID of the scene enabled: Enable/disable bloom strength: Bloom intensity (0.0-3.0, default: 1.0) radius: Bloom spread/blur radius (0.0-1.0, default: 0.4) threshold: Brightness threshold to trigger bloom (0.0-1.0, default: 0.8) Returns: Dictionary with bloom settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "post_processing" not in scene: scene["post_processing"] = {} bloom = { "enabled": enabled, "strength": max(0.0, min(3.0, strength)), "radius": max(0.0, min(1.0, radius)), "threshold": max(0.0, min(1.0, threshold)) } scene["post_processing"]["bloom"] = bloom storage.save(scene) if enabled: message = f"Enabled bloom (strength={strength}, radius={radius}, threshold={threshold})" else: message = "Disabled bloom" return { "scene_id": scene_id, "message": message, "bloom": bloom } def set_ssao( scene_id: str, enabled: bool, radius: float = 0.5, intensity: float = 1.0, bias: float = 0.025 ) -> Dict[str, Any]: """ Configure Screen Space Ambient Occlusion (SSAO). SSAO adds soft shadows in corners and crevices where ambient light would naturally be occluded, adding depth and realism. Args: scene_id: ID of the scene enabled: Enable/disable SSAO radius: Sample radius in world units (0.1-2.0, default: 0.5) intensity: Shadow intensity (0.0-2.0, default: 1.0) bias: Depth bias to prevent self-occlusion (0.001-0.1, default: 0.025) Returns: Dictionary with SSAO settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "post_processing" not in scene: scene["post_processing"] = {} ssao = { "enabled": enabled, "radius": max(0.1, min(2.0, radius)), "intensity": max(0.0, min(2.0, intensity)), "bias": max(0.001, min(0.1, bias)) } scene["post_processing"]["ssao"] = ssao storage.save(scene) if enabled: message = f"Enabled SSAO (radius={radius}, intensity={intensity})" else: message = "Disabled SSAO" return { "scene_id": scene_id, "message": message, "ssao": ssao } def set_color_grading( scene_id: str, enabled: bool, brightness: float = 0.0, contrast: float = 1.0, saturation: float = 1.0, hue: float = 0.0, exposure: float = 1.0, gamma: float = 1.0 ) -> Dict[str, Any]: """ Configure color grading post-processing. Adjust overall image colors for cinematic looks or stylized effects. Args: scene_id: ID of the scene enabled: Enable/disable color grading brightness: Brightness adjustment (-1.0 to 1.0, default: 0.0) contrast: Contrast multiplier (0.0-2.0, default: 1.0) saturation: Color saturation (0.0=grayscale, 1.0=normal, 2.0=vivid) hue: Hue shift in degrees (-180 to 180, default: 0) exposure: Exposure adjustment (0.0-3.0, default: 1.0) gamma: Gamma correction (0.5-2.5, default: 1.0) Returns: Dictionary with color grading settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "post_processing" not in scene: scene["post_processing"] = {} color_grading = { "enabled": enabled, "brightness": max(-1.0, min(1.0, brightness)), "contrast": max(0.0, min(2.0, contrast)), "saturation": max(0.0, min(2.0, saturation)), "hue": max(-180, min(180, hue)), "exposure": max(0.0, min(3.0, exposure)), "gamma": max(0.5, min(2.5, gamma)) } scene["post_processing"]["color_grading"] = color_grading storage.save(scene) if enabled: adjustments = [] if brightness != 0.0: adjustments.append(f"brightness={brightness}") if contrast != 1.0: adjustments.append(f"contrast={contrast}") if saturation != 1.0: adjustments.append(f"saturation={saturation}") if hue != 0.0: adjustments.append(f"hue={hue}°") if exposure != 1.0: adjustments.append(f"exposure={exposure}") if gamma != 1.0: adjustments.append(f"gamma={gamma}") if adjustments: message = f"Enabled color grading ({', '.join(adjustments)})" else: message = "Enabled color grading (default settings)" else: message = "Disabled color grading" return { "scene_id": scene_id, "message": message, "color_grading": color_grading } def set_vignette( scene_id: str, enabled: bool, intensity: float = 0.5, smoothness: float = 0.5 ) -> Dict[str, Any]: """ Configure vignette effect (darkened edges). Vignette darkens the corners and edges of the screen, drawing focus to the center of the image. Args: scene_id: ID of the scene enabled: Enable/disable vignette intensity: Darkness of the vignette (0.0-1.0, default: 0.5) smoothness: Softness of the vignette edge (0.0-1.0, default: 0.5) Returns: Dictionary with vignette settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "post_processing" not in scene: scene["post_processing"] = {} vignette = { "enabled": enabled, "intensity": max(0.0, min(1.0, intensity)), "smoothness": max(0.0, min(1.0, smoothness)) } scene["post_processing"]["vignette"] = vignette storage.save(scene) if enabled: message = f"Enabled vignette (intensity={intensity}, smoothness={smoothness})" else: message = "Disabled vignette" return { "scene_id": scene_id, "message": message, "vignette": vignette } def get_post_processing(scene_id: str) -> Dict[str, Any]: """ Get all post-processing settings for the scene. Args: scene_id: ID of the scene Returns: Dictionary with all post-processing settings """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") post_processing = scene.get("post_processing", {}) return { "scene_id": scene_id, "post_processing": post_processing } # ============================================================================= # Camera Effects Tools # ============================================================================= def set_depth_of_field( scene_id: str, enabled: bool, focus_distance: float = 10.0, aperture: float = 0.025, max_blur: float = 0.01 ) -> Dict[str, Any]: """ Configure depth of field (DoF) camera effect. Depth of field blurs objects that are not at the focus distance, simulating how real camera lenses focus on a specific plane. Args: scene_id: ID of the scene enabled: Enable/disable depth of field focus_distance: Distance to the focal plane in units (default: 10.0) aperture: Aperture size, affects blur amount (0.001-0.1, default: 0.025) max_blur: Maximum blur strength (0.0-0.05, default: 0.01) Returns: Dictionary with DoF settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "camera_effects" not in scene: scene["camera_effects"] = {} dof = { "enabled": enabled, "focus_distance": max(0.1, focus_distance), "aperture": max(0.001, min(0.1, aperture)), "max_blur": max(0.0, min(0.05, max_blur)) } scene["camera_effects"]["depth_of_field"] = dof storage.save(scene) if enabled: message = f"Enabled depth of field (focus={focus_distance}m, aperture={aperture})" else: message = "Disabled depth of field" return { "scene_id": scene_id, "message": message, "depth_of_field": dof } def set_motion_blur( scene_id: str, enabled: bool, intensity: float = 0.5, samples: int = 8 ) -> Dict[str, Any]: """ Configure motion blur camera effect. Motion blur adds blur in the direction of camera or object movement, creating a sense of speed and smooth motion. Args: scene_id: ID of the scene enabled: Enable/disable motion blur intensity: Blur intensity (0.0-2.0, default: 0.5) samples: Quality samples for blur (4-32, default: 8) Returns: Dictionary with motion blur settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "camera_effects" not in scene: scene["camera_effects"] = {} motion_blur = { "enabled": enabled, "intensity": max(0.0, min(2.0, intensity)), "samples": max(4, min(32, samples)) } scene["camera_effects"]["motion_blur"] = motion_blur storage.save(scene) if enabled: message = f"Enabled motion blur (intensity={intensity}, samples={samples})" else: message = "Disabled motion blur" return { "scene_id": scene_id, "message": message, "motion_blur": motion_blur } def set_chromatic_aberration( scene_id: str, enabled: bool, intensity: float = 0.005 ) -> Dict[str, Any]: """ Configure chromatic aberration effect. Chromatic aberration simulates lens imperfection by separating color channels at the edges of the screen. Args: scene_id: ID of the scene enabled: Enable/disable chromatic aberration intensity: Effect strength (0.0-0.05, default: 0.005) Returns: Dictionary with chromatic aberration settings and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "camera_effects" not in scene: scene["camera_effects"] = {} chromatic = { "enabled": enabled, "intensity": max(0.0, min(0.05, intensity)) } scene["camera_effects"]["chromatic_aberration"] = chromatic storage.save(scene) if enabled: message = f"Enabled chromatic aberration (intensity={intensity})" else: message = "Disabled chromatic aberration" return { "scene_id": scene_id, "message": message, "chromatic_aberration": chromatic } def get_camera_effects(scene_id: str) -> Dict[str, Any]: """ Get all camera effects settings for the scene. Args: scene_id: ID of the scene Returns: Dictionary with all camera effects settings """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") camera_effects = scene.get("camera_effects", {}) return { "scene_id": scene_id, "camera_effects": camera_effects } # ============================================================================= # Toon/Cel Shading Tools # ============================================================================= def update_material_to_toon( scene_id: str, object_id: str, enabled: bool = True, color: Optional[str] = None, gradient_steps: int = 3, outline: bool = True, outline_color: str = "#000000", outline_thickness: float = 0.03 ) -> Dict[str, Any]: """ Convert an object's material to toon/cel-shaded style. Toon shading creates a cartoon-like appearance with discrete shading bands instead of smooth gradients, commonly used in anime and cel-animation styles. Args: scene_id: ID of the scene object_id: ID of the object to update enabled: Enable/disable toon shading (True to apply, False to revert to standard) color: Base color for toon material (optional, keeps existing if not set) gradient_steps: Number of shading steps (2=hard, 3=medium, 5=soft, default: 3) outline: Add black outline effect (default: True) outline_color: Color of the outline (default: "#000000") outline_thickness: Thickness of outline (0.01-0.1, default: 0.03) Returns: Dictionary with updated material info and message """ scene = storage.get(scene_id) if not scene: raise ValueError(f"Scene '{scene_id}' not found") if "objects" not in scene or not scene["objects"]: raise ValueError("Scene has no objects") # Find object obj = None for o in scene["objects"]: if o.get("id") == object_id or o.get("object_id") == object_id: obj = o break if not obj: raise ValueError(f"Object '{object_id}' not found in scene") # Ensure material exists if "material" not in obj: obj["material"] = {} # Update toon properties if enabled: obj["material"]["toon"] = { "enabled": True, "gradient_steps": max(2, min(10, gradient_steps)), "outline": outline, "outline_color": outline_color, "outline_thickness": max(0.01, min(0.1, outline_thickness)) } if color: obj["material"]["color"] = color message = f"Applied toon shading to '{object_id}' ({gradient_steps} steps{', with outline' if outline else ''})" else: obj["material"]["toon"] = {"enabled": False} message = f"Disabled toon shading on '{object_id}' (reverted to standard material)" storage.save(scene) return { "scene_id": scene_id, "object_id": object_id, "message": message, "material": obj["material"] }