Advertisement
Guest User

Untitled

a guest
Jan 2nd, 2025
23
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 14.89 KB | None | 0 0
  1. #!/usr/bin/env python3
  2.  
  3. import math
  4. import time
  5. import random
  6.  
  7. def create_image(width, height):
  8. #Create an empty image (list of lists) initialized to black
  9. return [[(0.0, 0.0, 0.0) for _ in range(width)] for _ in range(height)]
  10.  
  11. def put_pixel(img, x, y, color):
  12. #Safely place a pixel into the image buffer, if in range.
  13. if 0 <= x < len(img[0]) and 0 <= y < len(img):
  14. img[y][x] = color
  15.  
  16. def clamp_color(c):
  17. #Clamp an (r, g, b) to integer 0..255
  18. return (
  19. max(0, min(255, int(c[0]))),
  20. max(0, min(255, int(c[1]))),
  21. max(0, min(255, int(c[2]))),
  22. )
  23.  
  24. def normalize(v):
  25. #Normalize a 3D vector.
  26. length = math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2])
  27. if length == 0:
  28. return (0, 0, 0)
  29. return (v[0]/length, v[1]/length, v[2]/length)
  30.  
  31. def vector_sub(a, b):
  32. #Subtract 3D vectors.
  33. return (a[0]-b[0], a[1]-b[1], a[2]-b[2])
  34.  
  35. def vector_add(a, b):
  36. #Add 3D vectors.
  37. return (a[0]+b[0], a[1]+b[1], a[2]+b[2])
  38.  
  39. def dot(a, b):
  40. #Dot product.
  41. return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
  42.  
  43. def mul_scalar(v, s):
  44. #Multiply a vector by a scalar.
  45. return (v[0]*s, v[1]*s, v[2]*s)
  46.  
  47.  
  48. # 1. Procedural Terrain (Static + Dynamic Updates)
  49. def generate_terrain(width, height, scale):
  50. #Create a 2D heightmap using a simple wave function.
  51. terrain = [[0.0 for _ in range(width)] for _ in range(height)]
  52. for y in range(height):
  53. for x in range(width):
  54. nx = x / scale
  55. ny = y / scale
  56. val = math.sin(nx) * math.cos(ny) * 10.0
  57. terrain[y][x] = val
  58. return terrain
  59.  
  60. def update_terrain(terrain, particles, erosion_rate):
  61. #Erode the terrain based on particle positions.
  62. #If a particle is over a certain cell, lower the height a bit.
  63. h = len(terrain)
  64. w = len(terrain[0]) if h > 0 else 0
  65. for p in particles:
  66. px = int(p[0] % w)
  67. py = int(p[1] % h)
  68. old_val = terrain[py][px]
  69. new_val = old_val - erosion_rate
  70. if new_val < 0:
  71. new_val = 0
  72. terrain[py][px] = new_val
  73.  
  74.  
  75. # 2. Particle Simulation (with Terrain Collision)
  76. def simulate_particles(num_particles, bounds, terrain, steps):
  77. #Particles bounce off terrain and walls.
  78. #Positions also used later for lighting/fog calculation.
  79. particles = []
  80. w, h, z_bound = bounds
  81.  
  82. # Init
  83. for _ in range(num_particles):
  84. x = random.uniform(0, w)
  85. y = random.uniform(0, h)
  86. z = random.uniform(-z_bound, z_bound)
  87. vx = random.uniform(-1, 1)
  88. vy = random.uniform(-1, 1)
  89. vz = random.uniform(-1, 1)
  90. particles.append([x, y, z, vx, vy, vz])
  91.  
  92. terrain_h = len(terrain)
  93. terrain_w = len(terrain[0]) if terrain_h > 0 else 0
  94.  
  95. for _ in range(steps):
  96. for p in particles:
  97. p[0] += p[3]
  98. p[1] += p[4]
  99. p[2] += p[5]
  100.  
  101. # Terrain collision
  102. tx = int(p[0] % terrain_w)
  103. ty = int(p[1] % terrain_h)
  104. th = terrain[ty][tx]
  105. if p[2] <= th:
  106. p[2] = th
  107. p[5] *= -1 # Bounce in z direction
  108.  
  109. # Reflect off bounding walls
  110. # X bound
  111. if p[0] < 0:
  112. p[0] = 0
  113. p[3] *= -1
  114. elif p[0] > w:
  115. p[0] = w
  116. p[3] *= -1
  117. # Y bound
  118. if p[1] < 0:
  119. p[1] = 0
  120. p[4] *= -1
  121. elif p[1] > h:
  122. p[1] = h
  123. p[4] *= -1
  124. # Z bound
  125. if p[2] < -z_bound:
  126. p[2] = -z_bound
  127. p[5] *= -1
  128. elif p[2] > z_bound:
  129. p[2] = z_bound
  130. p[5] *= -1
  131.  
  132. return particles
  133.  
  134.  
  135. # 3. Cloud Map
  136. def generate_cloud_map(width, height, swirl_scale):
  137. #Generate a simple swirl-based 'cloud' overlay in [0..1].
  138. #Each pixel has a swirl_value that we can use to blend in final color.
  139.  
  140. cloud_data = [[0.0 for _ in range(width)] for _ in range(height)]
  141. for j in range(height):
  142. for i in range(width):
  143. x = i / swirl_scale
  144. y = j / swirl_scale
  145. swirl_val = math.sin(x + math.sin(y)) + math.cos(y - 0.5*math.sin(x))
  146. # Normalize swirl_val from about -2..2 to [0..1]
  147. normalized = (swirl_val + 2.0) / 4.0
  148. cloud_data[j][i] = normalized
  149. return cloud_data
  150.  
  151.  
  152. # 4. Mandelbrot Fractal (for overlay)
  153. def mandelbrot_value(x, y, max_iter):
  154. #Compute a Mandelbrot iteration ratio at point (x, y).
  155. zr, zi = 0.0, 0.0
  156. cr, ci = x, y
  157. iter_count = 0
  158. while (zr*zr + zi*zi) <= 4.0 and iter_count < max_iter:
  159. tmp = zr*zr - zi*zi + cr
  160. zi = 2.0*zr*zi + ci
  161. zr = tmp
  162. iter_count += 1
  163. return iter_count / max_iter
  164.  
  165. def generate_fractal_overlay(width, height, max_iter):
  166. #Generate a 2D float array in [0..1], storing the Mandelbrot ratio at each pixel.
  167. fractal_data = [[0.0 for _ in range(width)] for _ in range(height)]
  168. for j in range(height):
  169. for i in range(width):
  170. # Map (i, j) to a region in the complex plane
  171. xx = (i/width)*3.0 - 2.0
  172. yy = (j/height)*2.0 - 1.0
  173. val = mandelbrot_value(xx, yy, max_iter)
  174. fractal_data[j][i] = val
  175. return fractal_data
  176.  
  177.  
  178. # 5. Ray Tracer with Reflections, Fog, Particle Lighting,
  179. # Terrain in background, Fractal overlay, Cloud overlay
  180. def intersect_sphere(ray_orig, ray_dir, center, radius):
  181. #Return the distance t along ray_dir where the ray hits the sphere, or None.
  182. ox, oy, oz = ray_orig
  183. cx, cy, cz = center
  184. oc = (ox-cx, oy-cy, oz-cz)
  185. a = dot(ray_dir, ray_dir)
  186. b = 2.0 * dot(oc, ray_dir)
  187. c = dot(oc, oc) - radius*radius
  188. disc = b*b - 4*a*c
  189. if disc < 0:
  190. return None
  191. sqrt_d = math.sqrt(disc)
  192. t1 = (-b - sqrt_d) / (2*a)
  193. t2 = (-b + sqrt_d) / (2*a)
  194. if t1 > 0 and (t1 < t2 or t2 < 0):
  195. return t1
  196. if t2 > 0 and (t2 < t1 or t1 < 0):
  197. return t2
  198. return None
  199.  
  200. def compute_reflection(ray_dir, normal):
  201. #Reflect the ray_dir around the normal: R = D - 2(D·N)N
  202. dot_dn = dot(ray_dir, normal)
  203. return vector_sub(ray_dir, mul_scalar(normal, 2.0 * dot_dn))
  204.  
  205. def compute_diffuse_light(point, normal, particles):
  206. #Sum diffuse contributions from each particle acting as a 'light source'.
  207. color_sum = (0.0, 0.0, 0.0)
  208. for p in particles:
  209. # Light color depends on particle velocity, for variety
  210. speed = math.sqrt(p[3]*p[3] + p[4]*p[4] + p[5]*p[5])
  211. # For instance: (r ~ speed, g ~ inversely ~ speed, b ~ 0.5)
  212. light_col = (min(1, speed/3.0), max(0, 1.0 - speed/3.0), 0.5)
  213.  
  214. light_dir = normalize((p[0] - point[0], p[1] - point[1], p[2] - point[2]))
  215. diff = max(0.0, dot(light_dir, normal))
  216. color_sum = (
  217. color_sum[0] + light_col[0]*diff,
  218. color_sum[1] + light_col[1]*diff,
  219. color_sum[2] + light_col[2]*diff,
  220. )
  221. return color_sum
  222.  
  223.  
  224. def compute_fog(color, dist, fog_density):
  225. #Exponential fog factor based on travel distance.
  226. fog_factor = math.exp(-fog_density*dist)
  227. # Fog color is a simple gray
  228. fog_col = (0.5, 0.5, 0.5)
  229. return (
  230. color[0]*fog_factor + fog_col[0]*(1 - fog_factor),
  231. color[1]*fog_factor + fog_col[1]*(1 - fog_factor),
  232. color[2]*fog_factor + fog_col[2]*(1 - fog_factor),
  233. )
  234.  
  235. def trace_ray(ray_orig, ray_dir, spheres, particles, depth=0):
  236. #Trace a single ray with optional reflection bounce.
  237.  
  238. closest_t = float("inf")
  239. hit_sphere = None
  240. for center, radius, color in spheres:
  241. t = intersect_sphere(ray_orig, ray_dir, center, radius)
  242. if t is not None and t < closest_t:
  243. closest_t = t
  244. hit_sphere = (center, radius, color)
  245.  
  246. if hit_sphere is None:
  247. # No intersection => background
  248. return (0.0, 0.0, 0.0), None
  249.  
  250. hit_point = vector_add(ray_orig, mul_scalar(ray_dir, closest_t))
  251. normal = normalize(vector_sub(hit_point, hit_sphere[0]))
  252.  
  253. # Basic diffuse from particle lights
  254. diffuse = compute_diffuse_light(hit_point, normal, particles)
  255. base_color = (
  256. hit_sphere[2][0] * diffuse[0],
  257. hit_sphere[2][1] * diffuse[1],
  258. hit_sphere[2][2] * diffuse[2],
  259. )
  260.  
  261. # Reflection
  262. if depth < 1: # Single reflection bounce
  263. reflect_dir = normalize(compute_reflection(ray_dir, normal))
  264. reflected_col, _ = trace_ray(hit_point, reflect_dir, spheres, particles, depth+1)
  265. # Blend reflection
  266. reflection_factor = 0.3
  267. base_color = (
  268. base_color[0]*(1 - reflection_factor) + reflected_col[0]*reflection_factor,
  269. base_color[1]*(1 - reflection_factor) + reflected_col[1]*reflection_factor,
  270. base_color[2]*(1 - reflection_factor) + reflected_col[2]*reflection_factor,
  271. )
  272.  
  273. return base_color, closest_t
  274.  
  275.  
  276. # 6. Depth of Field (Naive Implementation)
  277. def camera_rays_with_dof(i, j, width, height, fov, aspect_ratio, focal_length, aperture, samples):
  278.  
  279. #Generate multiple rays for naive Depth of Field effect.
  280. #Each sample uses a random offset in camera-aperture space.
  281.  
  282. # Basic pinhole without DoF: direction = normalize((x, y, -1))
  283. # We'll gather multiple rays with random offsets in [aperture].
  284. angle = math.tan(math.pi * 0.5 * fov / 180.0)
  285.  
  286. # Pixel center in camera space
  287. px = (2.0*((i+0.5)/float(width)) - 1.0)*angle*aspect_ratio
  288. py = (1.0 - 2.0*((j+0.5)/float(height)))*angle
  289.  
  290. # We'll treat the camera as located at (0,0,0) looking down -Z.
  291. # The focal plane is at z = -focal_length.
  292. # We'll randomize the origin in a small lens circle for each sample.
  293. # Then we define the direction to the focal point, which is (px, py, -1)*focal_length.
  294.  
  295. rays = []
  296. for _ in range(samples):
  297. # Random offset in lens space
  298. lens_x = (random.random() - 0.5) * aperture
  299. lens_y = (random.random() - 0.5) * aperture
  300.  
  301. # Focal point
  302. focal_point = (px * focal_length, py * focal_length, -focal_length)
  303.  
  304. # Ray origin offset
  305. origin = (lens_x, lens_y, 0.0)
  306. dir_approx = vector_sub(focal_point, origin)
  307. dir_norm = normalize(dir_approx)
  308. rays.append((origin, dir_norm))
  309.  
  310. return rays
  311.  
  312.  
  313. # 7. Final Raytrace Scene with DoF
  314. def raytrace_scene_with_dof(
  315. img, terrain, particles,
  316. fractal_img, cloud_map,
  317. width, height
  318. ):
  319. #Perform ray tracing with Depth of Field, fractal overlay, cloud overlay, etc.
  320. # Hardcode some spheres
  321. spheres = [
  322. ((0, 0, -20), 4, (1, 0, 0)), # Red
  323. ((5, -1, -15), 2, (0, 1, 0)), # Green
  324. ((-4, 2, -18), 3, (0.2, 0.6, 1)) # Blue-ish
  325. ]
  326.  
  327. fov = 60.0
  328. aspect_ratio = width / float(height)
  329. fog_density = 0.02
  330.  
  331. # Depth of Field parameters
  332. focal_length = 1.5
  333. aperture = 0.1
  334. samples_per_pixel = 4 # More samples => heavier computation
  335.  
  336. for j in range(height):
  337. for i in range(width):
  338. final_col = (0.0, 0.0, 0.0)
  339. # Generate multiple rays for DoF
  340. rays = camera_rays_with_dof(i, j, width, height, fov, aspect_ratio, focal_length, aperture, samples_per_pixel)
  341. for origin, direction in rays:
  342. color, dist = trace_ray(origin, direction, spheres, particles, depth=0)
  343. if dist is None:
  344. dist = 9999.0
  345.  
  346. # Fog
  347. color = compute_fog(color, dist, fog_density)
  348.  
  349. # Fractal overlay
  350. fractal_c = fractal_img[j][i] # 0..1
  351. color = (
  352. color[0]*fractal_c,
  353. color[1]*fractal_c,
  354. color[2]*fractal_c
  355. )
  356.  
  357. # Cloud overlay
  358. cloud_c = cloud_map[j][i] # 0..1
  359. color = (
  360. color[0]*(1 - cloud_c) + 1.0*cloud_c,
  361. color[1]*(1 - cloud_c) + 1.0*cloud_c,
  362. color[2]*(1 - cloud_c) + 1.0*cloud_c
  363. )
  364.  
  365. # Accumulate
  366. final_col = (
  367. final_col[0] + color[0],
  368. final_col[1] + color[1],
  369. final_col[2] + color[2],
  370. )
  371. # Average
  372. final_col = (
  373. final_col[0]/samples_per_pixel,
  374. final_col[1]/samples_per_pixel,
  375. final_col[2]/samples_per_pixel,
  376. )
  377. put_pixel(img, i, j, final_col)
  378.  
  379. def main():
  380. width, height = 300, 300
  381.  
  382. # Terrain + Particle sim bounds
  383. terrain_scale = 50
  384. erosion_rate = 0.01
  385. num_particles = 100
  386. bounds = (width, height, 20)
  387. particle_steps = 200
  388.  
  389. # For fractal and clouds
  390. fractal_iter = 100
  391. swirl_scale = 80.0
  392.  
  393. random.seed(42) # For reproducibility
  394.  
  395. start_all = time.time()
  396.  
  397. # 1) Generate terrain
  398. start_terrain = time.time()
  399. terrain = generate_terrain(width, height, terrain_scale)
  400. end_terrain = time.time()
  401.  
  402. # 2) Simulate particles
  403. start_particles = time.time()
  404. particles = simulate_particles(num_particles, bounds, terrain, particle_steps)
  405. end_particles = time.time()
  406.  
  407. # 2.5) Erode terrain after simulation
  408. update_terrain(terrain, particles, erosion_rate)
  409.  
  410. # 3) Generate cloud map
  411. start_clouds = time.time()
  412. cloud_map = generate_cloud_map(width, height, swirl_scale)
  413. end_clouds = time.time()
  414.  
  415. # 4) Generate fractal overlay
  416. start_fractal = time.time()
  417. fractal_img = generate_fractal_overlay(width, height, fractal_iter)
  418. end_fractal = time.time()
  419.  
  420. # 5) Ray Trace scene (with Depth of Field)
  421. start_ray = time.time()
  422. img = create_image(width, height)
  423. raytrace_scene_with_dof(
  424. img, terrain, particles,
  425. fractal_img, cloud_map,
  426. width, height
  427. )
  428. end_ray = time.time()
  429.  
  430. end_all = time.time()
  431.  
  432. # Print times
  433. print("=== TIMING ===")
  434. print(f"Terrain generation: {end_terrain - start_terrain:.2f} s")
  435. print(f"Particle simulation: {end_particles - start_particles:.2f} s")
  436. print(f"Cloud generation: {end_clouds - start_clouds:.2f} s")
  437. print(f"Fractal generation: {end_fractal - start_fractal:.2f} s")
  438. print(f"Ray tracing (with DoF): {end_ray - start_ray:.2f} s")
  439. print(f"TOTAL: {end_all - start_all:.2f} s")
  440.  
  441. # Save final output
  442. print("Saving final image to 'final_output.ppm'...")
  443. with open("final_output.ppm", "w") as f:
  444. f.write(f"P3\n{width} {height}\n255\n")
  445. for row in img:
  446. for (r_f, g_f, b_f) in row:
  447. (r, g, b) = clamp_color((r_f*255, g_f*255, b_f*255))
  448. f.write(f"{r} {g} {b} ")
  449. f.write("\n")
  450.  
  451. print("Done!")
  452.  
  453. if __name__ == "__main__":
  454. main()
  455.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement