|
|
""" |
|
|
Environment Tools |
|
|
Skybox, particles, and other environmental effects |
|
|
""" |
|
|
from typing import Dict, Any, Optional |
|
|
from backend.storage import storage |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_skybox( |
|
|
scene_id: str, |
|
|
preset: str = "day", |
|
|
turbidity: float = 10.0, |
|
|
rayleigh: float = 2.0, |
|
|
sun_elevation: float = 45.0, |
|
|
sun_azimuth: float = 180.0 |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
Add a procedural sky to the scene using Three.js Sky shader. |
|
|
|
|
|
The sky simulates atmospheric scattering for realistic outdoor environments. |
|
|
|
|
|
Args: |
|
|
scene_id: ID of the scene |
|
|
preset: Quick preset - "day", "sunset", "noon", "dawn", "night" (overrides other params) |
|
|
turbidity: Haziness of the atmosphere (2.0=clear, 10.0=hazy, 20.0=foggy) |
|
|
rayleigh: Amount of Rayleigh scattering (affects blue color of sky) |
|
|
sun_elevation: Sun angle from horizon in degrees (0=horizon, 90=overhead) |
|
|
sun_azimuth: Sun compass direction in degrees (0=north, 90=east, 180=south) |
|
|
|
|
|
Returns: |
|
|
Dictionary with skybox 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"] = {} |
|
|
|
|
|
|
|
|
presets = { |
|
|
"day": {"turbidity": 10.0, "rayleigh": 2.0, "sun_elevation": 45.0, "sun_azimuth": 180.0}, |
|
|
"noon": {"turbidity": 8.0, "rayleigh": 1.5, "sun_elevation": 85.0, "sun_azimuth": 180.0}, |
|
|
"sunset": {"turbidity": 4.0, "rayleigh": 4.0, "sun_elevation": 5.0, "sun_azimuth": 180.0}, |
|
|
"dawn": {"turbidity": 4.0, "rayleigh": 4.0, "sun_elevation": 5.0, "sun_azimuth": 0.0}, |
|
|
"night": {"turbidity": 20.0, "rayleigh": 0.1, "sun_elevation": -10.0, "sun_azimuth": 180.0}, |
|
|
} |
|
|
|
|
|
if preset in presets: |
|
|
params = presets[preset] |
|
|
turbidity = params["turbidity"] |
|
|
rayleigh = params["rayleigh"] |
|
|
sun_elevation = params["sun_elevation"] |
|
|
sun_azimuth = params["sun_azimuth"] |
|
|
|
|
|
|
|
|
turbidity = max(0.1, min(20.0, turbidity)) |
|
|
rayleigh = max(0.0, min(4.0, rayleigh)) |
|
|
sun_elevation = max(-90.0, min(90.0, sun_elevation)) |
|
|
sun_azimuth = sun_azimuth % 360.0 |
|
|
|
|
|
skybox = { |
|
|
"enabled": True, |
|
|
"type": "procedural", |
|
|
"turbidity": turbidity, |
|
|
"rayleigh": rayleigh, |
|
|
"sun_elevation": sun_elevation, |
|
|
"sun_azimuth": sun_azimuth, |
|
|
"preset": preset |
|
|
} |
|
|
|
|
|
scene["environment"]["skybox"] = skybox |
|
|
storage.save(scene) |
|
|
|
|
|
return { |
|
|
"scene_id": scene_id, |
|
|
"message": f"Added {preset} sky (sun at {sun_elevation}° elevation)", |
|
|
"skybox": skybox |
|
|
} |
|
|
|
|
|
|
|
|
def remove_skybox(scene_id: str) -> Dict[str, Any]: |
|
|
""" |
|
|
Remove the skybox from the scene, reverting to solid background color. |
|
|
|
|
|
Args: |
|
|
scene_id: ID of the scene |
|
|
|
|
|
Returns: |
|
|
Dictionary with confirmation message |
|
|
""" |
|
|
scene = storage.get(scene_id) |
|
|
if not scene: |
|
|
raise ValueError(f"Scene '{scene_id}' not found") |
|
|
|
|
|
if "environment" not in scene: |
|
|
scene["environment"] = {} |
|
|
|
|
|
|
|
|
had_skybox = scene["environment"].get("skybox", {}).get("enabled", False) |
|
|
|
|
|
scene["environment"]["skybox"] = {"enabled": False} |
|
|
storage.save(scene) |
|
|
|
|
|
if had_skybox: |
|
|
return { |
|
|
"scene_id": scene_id, |
|
|
"message": "Removed skybox, reverted to solid background color" |
|
|
} |
|
|
else: |
|
|
return { |
|
|
"scene_id": scene_id, |
|
|
"message": "No skybox was active" |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_particles( |
|
|
scene_id: str, |
|
|
preset: str, |
|
|
position: Optional[Dict[str, float]] = None, |
|
|
particle_id: Optional[str] = None |
|
|
) -> Dict[str, Any]: |
|
|
""" |
|
|
Add a particle effect to the scene using preset configurations. |
|
|
|
|
|
Args: |
|
|
scene_id: ID of the scene |
|
|
preset: Effect type - "fire", "smoke", "sparkle", "rain", "snow" |
|
|
position: {x, y, z} position for localized effects (fire, smoke, sparkle) |
|
|
For weather effects (rain, snow), position is ignored (covers world) |
|
|
particle_id: Optional unique identifier for this particle system |
|
|
|
|
|
Returns: |
|
|
Dictionary with particle system info and message |
|
|
""" |
|
|
scene = storage.get(scene_id) |
|
|
if not scene: |
|
|
raise ValueError(f"Scene '{scene_id}' not found") |
|
|
|
|
|
valid_presets = ["fire", "smoke", "sparkle", "rain", "snow"] |
|
|
if preset not in valid_presets: |
|
|
raise ValueError(f"Invalid preset '{preset}'. Must be one of: {valid_presets}") |
|
|
|
|
|
if "particles" not in scene: |
|
|
scene["particles"] = [] |
|
|
|
|
|
|
|
|
if not particle_id: |
|
|
particle_id = f"{preset}_{len(scene['particles'])}" |
|
|
|
|
|
|
|
|
for p in scene["particles"]: |
|
|
if p.get("id") == particle_id: |
|
|
raise ValueError(f"Particle system with id '{particle_id}' already exists") |
|
|
|
|
|
|
|
|
if position is None: |
|
|
position = {"x": 0, "y": 1, "z": 0} |
|
|
|
|
|
|
|
|
preset_configs = { |
|
|
"fire": { |
|
|
"count": 200, |
|
|
"size": 0.3, |
|
|
"color_start": "#ff6600", |
|
|
"color_end": "#ff0000", |
|
|
"velocity": {"x": 0, "y": 2, "z": 0}, |
|
|
"lifetime": 1.5, |
|
|
"spread": 0.5, |
|
|
"localized": True |
|
|
}, |
|
|
"smoke": { |
|
|
"count": 100, |
|
|
"size": 0.8, |
|
|
"color_start": "#666666", |
|
|
"color_end": "#333333", |
|
|
"velocity": {"x": 0, "y": 1, "z": 0}, |
|
|
"lifetime": 3.0, |
|
|
"spread": 1.0, |
|
|
"localized": True |
|
|
}, |
|
|
"sparkle": { |
|
|
"count": 50, |
|
|
"size": 0.1, |
|
|
"color_start": "#ffffff", |
|
|
"color_end": "#ffff00", |
|
|
"velocity": {"x": 0, "y": 0.5, "z": 0}, |
|
|
"lifetime": 2.0, |
|
|
"spread": 2.0, |
|
|
"localized": True |
|
|
}, |
|
|
"rain": { |
|
|
"count": 1000, |
|
|
"size": 0.05, |
|
|
"color_start": "#aaccff", |
|
|
"color_end": "#88aadd", |
|
|
"velocity": {"x": 0, "y": -15, "z": 0}, |
|
|
"lifetime": 2.0, |
|
|
"spread": 50.0, |
|
|
"localized": False |
|
|
}, |
|
|
"snow": { |
|
|
"count": 500, |
|
|
"size": 0.15, |
|
|
"color_start": "#ffffff", |
|
|
"color_end": "#eeeeff", |
|
|
"velocity": {"x": 0.5, "y": -2, "z": 0.3}, |
|
|
"lifetime": 8.0, |
|
|
"spread": 50.0, |
|
|
"localized": False |
|
|
} |
|
|
} |
|
|
|
|
|
config = preset_configs[preset] |
|
|
|
|
|
particle_system = { |
|
|
"id": particle_id, |
|
|
"preset": preset, |
|
|
"position": position, |
|
|
"enabled": True, |
|
|
**config |
|
|
} |
|
|
|
|
|
scene["particles"].append(particle_system) |
|
|
storage.save(scene) |
|
|
|
|
|
if config["localized"]: |
|
|
pos_str = f" at ({position['x']}, {position['y']}, {position['z']})" |
|
|
else: |
|
|
pos_str = " (world-wide weather effect)" |
|
|
|
|
|
return { |
|
|
"scene_id": scene_id, |
|
|
"particle_id": particle_id, |
|
|
"message": f"Added {preset} particle effect{pos_str}", |
|
|
"particle_system": particle_system |
|
|
} |
|
|
|
|
|
|
|
|
def remove_particles(scene_id: str, particle_id: str) -> Dict[str, Any]: |
|
|
""" |
|
|
Remove a particle system from the scene. |
|
|
|
|
|
Args: |
|
|
scene_id: ID of the scene |
|
|
particle_id: ID of the particle system to remove |
|
|
|
|
|
Returns: |
|
|
Dictionary with confirmation message |
|
|
""" |
|
|
scene = storage.get(scene_id) |
|
|
if not scene: |
|
|
raise ValueError(f"Scene '{scene_id}' not found") |
|
|
|
|
|
if "particles" not in scene or not scene["particles"]: |
|
|
raise ValueError("Scene has no particle systems") |
|
|
|
|
|
original_count = len(scene["particles"]) |
|
|
scene["particles"] = [p for p in scene["particles"] if p.get("id") != particle_id] |
|
|
|
|
|
if len(scene["particles"]) == original_count: |
|
|
raise ValueError(f"Particle system '{particle_id}' not found") |
|
|
|
|
|
storage.save(scene) |
|
|
|
|
|
return { |
|
|
"scene_id": scene_id, |
|
|
"message": f"Removed particle system '{particle_id}'", |
|
|
"particle_id": particle_id |
|
|
} |
|
|
|