Advertisement
cookertron

Rotating 3D Sphere with Light Source in Python + Pygame

Nov 2nd, 2024
211
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 20.52 KB | Source Code | 0 0
  1. import pygame
  2. import numpy as np
  3.  
  4. # Initialize Pygame
  5. pygame.init()
  6.  
  7. # Set up the display
  8. WIDTH = 800
  9. HEIGHT = 600
  10. screen = pygame.display.set_mode((WIDTH, HEIGHT))
  11. pygame.display.set_caption("Optimized Rotating Sphere with Smooth Shading")
  12.  
  13. # Colors
  14. WHITE = (255, 255, 255)
  15. BLACK = (0, 0, 0)
  16. GREEN = (0, 255, 0)
  17. YELLOW = (255, 255, 0)
  18.  
  19. # Set up font for display
  20. font = pygame.font.Font(None, 24)
  21.  
  22. # Load point cloud data
  23. # Calculate the number of points per ring
  24. POINTS_PER_RING = 10
  25. NUM_RINGS = len(points) // POINTS_PER_RING
  26.  
  27. # Visualization parameters
  28. scale = 200
  29. z_offset = 500
  30. rotation_speed = 0.02
  31. point_size = 2
  32.  
  33. # Light parameters
  34. light_distance = 800  # Distance from center
  35. light_height = 400    # Height above sphere
  36. light_pos = np.array([light_distance, light_height, 0.])
  37. ambient_intensity = 0.2
  38. diffuse_intensity = 0.7
  39. specular_intensity = 0.3
  40. specular_power = 32
  41.  
  42. def create_triangles():
  43.     """Create triangle indices for connecting points"""
  44.     triangles = []
  45.     for ring in range(NUM_RINGS - 1):
  46.         for point in range(POINTS_PER_RING):
  47.             current = ring * POINTS_PER_RING + point
  48.             next_point = ring * POINTS_PER_RING + ((point + 1) % POINTS_PER_RING)
  49.             next_ring_current = (ring + 1) * POINTS_PER_RING + point
  50.             next_ring_next = (ring + 1) * POINTS_PER_RING + ((point + 1) % POINTS_PER_RING)
  51.            
  52.             # Original winding order might be inconsistent
  53.             # Let's define triangles with consistent counter-clockwise order
  54.             triangles.append([current, next_point, next_ring_next])
  55.             triangles.append([current, next_ring_next, next_ring_current])
  56.     return np.array(triangles)
  57.  
  58. triangles = create_triangles()
  59.  
  60. # Precompute vertex normals (normalized position vectors for a sphere)
  61. vertex_normals = points / np.linalg.norm(points, axis=1)[:, np.newaxis]
  62.  
  63. def rotate_y(points, angle):
  64.     """Rotate points around Y axis"""
  65.     cos_a = np.cos(angle)
  66.     sin_a = np.sin(angle)
  67.     rotation_matrix = np.array([
  68.         [cos_a, 0, sin_a],
  69.         [0, 1, 0],
  70.         [-sin_a, 0, cos_a]
  71.     ])
  72.     return np.dot(points, rotation_matrix.T)
  73.  
  74. def project_points(points, scale_factor):
  75.     """Project 3D points to 2D coordinates"""
  76.     x, y, z = (points * scale_factor).T
  77.     factor = z_offset / (z + z_offset)
  78.     x_projected = x * factor + WIDTH // 2
  79.     y_projected = y * factor + HEIGHT // 2
  80.     projected_points = np.stack([x_projected, y_projected], axis=-1)
  81.     return projected_points, z
  82.  
  83. def calculate_lighting(normals, vertex_positions, viewer_pos, light_pos):
  84.     """Vectorized calculation of lighting using Phong reflection model"""
  85.     # View direction
  86.     view_dir = viewer_pos - vertex_positions
  87.     view_dir /= np.linalg.norm(view_dir, axis=1)[:, np.newaxis]
  88.    
  89.     # Light direction
  90.     light_dir = light_pos - vertex_positions
  91.     light_dir /= np.linalg.norm(light_dir, axis=1)[:, np.newaxis]
  92.    
  93.     # Ambient component
  94.     ambient = ambient_intensity
  95.    
  96.     # Diffuse component
  97.     diff = np.maximum(0, np.einsum('ij,ij->i', normals, light_dir))
  98.     diffuse = diff * diffuse_intensity
  99.    
  100.     # Specular component
  101.     reflect_dir = 2 * np.einsum('ij,ij->i', normals, light_dir)[:, np.newaxis] * normals - light_dir
  102.     spec = np.maximum(0, np.einsum('ij,ij->i', view_dir, reflect_dir)) ** specular_power
  103.     specular = spec * specular_intensity
  104.    
  105.     # Total intensity
  106.     total_intensity = np.clip(ambient + diffuse + specular, 0, 1)
  107.     return total_intensity
  108.  
  109. def draw_shaded_mesh(screen, points, normals, triangles, scale_factor, light_pos, angle):
  110.     """Draw the triangular mesh with smooth shading"""
  111.     viewer_pos = np.array([0, 0, z_offset / scale_factor])
  112.     light_pos_scaled = light_pos / scale_factor
  113.  
  114.     # Project points
  115.     projected_points, z_values = project_points(points, scale_factor)
  116.  
  117.     # Calculate lighting per vertex
  118.     vertex_lighting = calculate_lighting(normals, points, viewer_pos, light_pos_scaled)
  119.  
  120.     # Prepare triangle data
  121.     triangle_indices = triangles
  122.     v0 = points[triangle_indices[:, 0]]
  123.     v1 = points[triangle_indices[:, 1]]
  124.     v2 = points[triangle_indices[:, 2]]
  125.     edge1 = v1 - v0
  126.     edge2 = v2 - v0
  127.     face_normals = np.cross(edge1, edge2)
  128.     face_normals /= np.linalg.norm(face_normals, axis=1)[:, np.newaxis]
  129.  
  130.     # Backface culling based on face normal Z component
  131.     # Since camera looks along negative Z, faces with normals pointing in +Z direction are culled
  132.     visible = face_normals[:, 2] <= 0
  133.  
  134.     # Filter visible triangles
  135.     triangle_indices = triangle_indices[visible]
  136.     triangle_z = -np.mean(z_values[triangle_indices], axis=1)
  137.     triangle_points_2d = projected_points[triangle_indices]
  138.     triangle_intensities = vertex_lighting[triangle_indices]
  139.  
  140.     # Compute triangle colors
  141.     triangle_colors = (np.mean(triangle_intensities, axis=1) * 255).astype(np.uint8)
  142.  
  143.     # Sort triangles by depth
  144.     sort_indices = np.argsort(triangle_z)
  145.  
  146.     # Draw triangles
  147.     for idx in sort_indices:
  148.         points_2d = triangle_points_2d[idx]
  149.         color_intensity = triangle_colors[idx]
  150.         color = (color_intensity, color_intensity, color_intensity)
  151.         pygame.draw.polygon(screen, color, points_2d.astype(int))
  152.  
  153.     # Draw light source
  154.     light_projected, _ = project_points(light_pos_scaled[np.newaxis, :], scale_factor)
  155.     pygame.draw.circle(screen, YELLOW, light_projected[0].astype(int), 5)
  156.  
  157.  
  158. # Initialize light parameters
  159. light_distance = 800
  160. light_height = 0
  161. light_pos = np.array([0., light_height, -light_distance])
  162. light_angle = 0.0
  163. light_orbit = True  # Ensure light orbit is enabled
  164.  
  165. # Game loop
  166. running = True
  167. angle = 0.0
  168. clock = pygame.time.Clock()
  169. paused = False
  170. show_points = True
  171. show_mesh = True
  172.  
  173. while running:
  174.     for event in pygame.event.get():
  175.         if event.type == pygame.QUIT:
  176.             running = False
  177.         elif event.type == pygame.KEYDOWN:
  178.             if event.key == pygame.K_SPACE:
  179.                 paused = not paused
  180.             elif event.key == pygame.K_UP:
  181.                 scale *= 1.1
  182.             elif event.key == pygame.K_DOWN:
  183.                 scale /= 1.1
  184.             elif event.key == pygame.K_LEFT:
  185.                 rotation_speed /= 1.2
  186.             elif event.key == pygame.K_RIGHT:
  187.                 rotation_speed *= 1.2
  188.             elif event.key == pygame.K_r:
  189.                 scale = 200
  190.                 rotation_speed = 0.02
  191.                 angle = 0
  192.                 light_distance = 800
  193.                 light_height = 0
  194.                 light_pos = np.array([0., light_height, -light_distance])
  195.                 light_angle = 0.0
  196.                 light_orbit = True  # Ensure light orbit is enabled                
  197.             elif event.key == pygame.K_p:
  198.                 show_points = not show_points
  199.             elif event.key == pygame.K_m:
  200.                 show_mesh = not show_mesh
  201.             elif event.key == pygame.K_l:
  202.                 light_orbit = not light_orbit
  203.             elif event.key == pygame.K_w:
  204.                 light_height += 50
  205.                 light_pos[1] = light_height
  206.             elif event.key == pygame.K_s:
  207.                 light_height -= 50
  208.                 light_pos[1] = light_height
  209.             elif event.key == pygame.K_a:
  210.                 if light_distance > 100:
  211.                     light_distance *= 0.9
  212.                     light_pos[2] = -light_distance
  213.             elif event.key == pygame.K_d:
  214.                 light_distance *= 1.1
  215.                 light_pos[2] = -light_distance
  216.  
  217.     # Clear screen
  218.     screen.fill(BLACK)
  219.  
  220.     # Rotate the points and normals
  221.     if not paused:
  222.         angle += rotation_speed
  223.         rotated_points = rotate_y(points, angle)
  224.         rotated_normals = rotate_y(vertex_normals, angle)
  225.         if light_orbit:
  226.             light_angle += rotation_speed * 5
  227.     else:
  228.         rotated_points = rotate_y(points, angle)
  229.         rotated_normals = rotate_y(vertex_normals, angle)
  230.  
  231.     # Update light position
  232.     if light_orbit:
  233.         # Rotate light around Y-axis
  234.         rotated_light = rotate_y(light_pos[np.newaxis, :], light_angle)[0]
  235.     else:
  236.         rotated_light = light_pos  # Use static position if not orbiting
  237.  
  238.     # Draw mesh
  239.     if show_mesh:
  240.         draw_shaded_mesh(screen, rotated_points, rotated_normals, triangles, scale, rotated_light, angle)
  241.  
  242.     # Draw points
  243.     if show_points:
  244.         projected_points, _ = project_points(rotated_points, scale)
  245.         for point in projected_points.astype(int):
  246.             pygame.draw.circle(screen, WHITE, point, point_size)
  247.  
  248.     # Display controls and info
  249.     info_texts = [
  250.         f"FPS: {int(clock.get_fps())}",
  251.         "Controls:",
  252.         "SPACE: Pause/Resume",
  253.         "UP/DOWN: Scale",
  254.         "LEFT/RIGHT: Rotation Speed",
  255.         "P: Toggle Points",
  256.         "M: Toggle Mesh",
  257.         "L: Toggle Light Orbit",
  258.         "R: Reset",
  259.         f"Scale: {scale:.1f}",
  260.         f"Rotation Speed: {rotation_speed:.3f}"
  261.     ]
  262.    
  263.     for i, text in enumerate(info_texts):
  264.         text_surface = font.render(text, True, GREEN)
  265.         screen.blit(text_surface, (10, 10 + i * 20))
  266.  
  267.     pygame.display.flip()
  268.     clock.tick(60)
  269.  
  270. pygame.quit()
  271.  
Tags: python pygame
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement