#!/usr/bin/python3 #------------------------- # A naive raycaster # # by Raattis@FlyWare.fi #------------------------- try: import pygame except: raise Exception('\n\nCould not import PyGame. In Windows download it from "http://www.pygame.org/download.shtml". In linux use your package manager of choice to install it (f.ex. aptitude: "sudo apt-get install pythonX.X-pygame" where "X.X" is your python version number).') import random, re, threading, sys from math import cos, sin, tan, pi, sqrt, atan, acos if '-h' in sys.argv: print( 'w,a,s,d,q,z flies around\n' 'up,down,left,right looks around\n' 'o brings up the settings\n' 'c and v make new objects\n' ) quit() #----------------------RENDERING CLASSES-------------------------- #----------------------CUBE CLASS-------------------------- class Cube(object): def __init__(self, x,y,z, w,h,d, color = (255,255,255)): if verbose: print("making cube: pos=({0},{1},{2}) size=({3},{4},{5})".format(x,y,z, w,h,d)) self.x, self.y, self.z = x,y,z self.w, self.h, self.d = w,h,d self.c = color def collidepoint(self, x, y, z): if verbose: profiler("point colliding",False) if between(x, self.x, self.x + self.w) and between(y, self.y, self.y + self.h) and between(z, self.z, self.z + self.d): if verbose: profiler("point colliding") return self.c if verbose: profiler("point colliding") return False def save_format(self): return 'cube: {0},{1},{2}, {3},{4},{5}, {6},{7},{8}'.format( self.x,self.y,self.z,self.w,self.h,self.d, self.c[0],self.c[1],self.c[2]) #----------------------SPHERE CLASS-------------------------- class Sphere(object): def __init__(self, x,y,z, radius, color = (255,255,255)): if verbose: print("making sphere: pos=({0},{1},{2}) radius={3}".format(x,y,z, radius)) self.x, self.y, self.z = x,y,z self.r = radius self.c = color def collidepoint(self, x, y, z): if verbose: profiler("sphere colliding",False) if sqrt((self.x - x)**2 +(self.y - y)**2 +(self.z - z)**2) < self.r: if verbose: profiler("sphere colliding") return self.c if verbose: profiler("sphere colliding") return False def save_format(self): return 'sphere: {0},{1},{2}, {3}, {4},{5},{6}'.format( self.x,self.y,self.z,self.r,self.c[0],self.c[1],self.c[2]) #----------------------CYLINDER CLASS-------------------------- class Cylinder(object): def __init__(self, x,y,z, length, radius, orientation, color = (255,255,255)): "Orientation: 'x', 'y', 'z'" if verbose: print("making cylinder: pos=({0},{1},{2}) l={3} r={4} orientation={5}".format(x,y,z, length,radius, orientation)) self.x, self.y, self.z = x,y,z self.l, self.r = length, radius if type(orientation) == str: self.o = {'x':0,'y':1,'z':2}[orientation] else: self.o = orientation self.c = color def collidepoint(self, x, y, z): if verbose: profiler("cylinder colliding",False) if self.o == 0 and not between(x, self.x, self.x + self.l): pass elif self.o == 1 and not between(y, self.y, self.y + self.l): pass elif self.o == 2 and not between(z, self.z, self.z + self.l): pass elif sqrt((self.o != 0)*(self.x - x)**2 +(self.o != 1)*(self.y - y)**2 +(self.o != 2)*(self.z - z)**2) < self.r: if verbose: profiler("cylinder colliding") return self.c if verbose: profiler("cylinder colliding") return False def save_format(self): return 'cylinder: {0},{1},{2}, {3},{4},{5}, {6},{7},{8}'.format( self.x,self.y,self.z,self.l,self.r,self.o,self.c[0],self.c[1],self.c[2]) #----------------------OBJECTGROUP CLASS-------------------------- class ObjectGroup(Cube): def __init__(self, x,y,z, w=1,h=1,d=1, render = [], cutout = []): if verbose: print("making ObjectGroup: pos=({0},{1},{2}) size=({3},{4},{5})".format(x,y,z, w,h,d)) self.x, self.y, self.z = x,y,z self.w, self.h, self.d = w,h,d self.rndr = render self.cuto = cutout def collidepoint(self, x, y, z): x, y, z = x-self.x, y-self.y, z-self.z #x, y, z = (x-self.x)/self.w, (y-self.y)/self.h, (z-self.z)/self.d for o in self.cuto: if o.collidepoint(x,y,z): return False for o in self.rndr: if o.collidepoint(x,y,z): return o.collidepoint(x,y,z) def save_format(self): s = 'group: {0},{1},{2}, {3},{4},{5}\n <'.format(self.x,self.y,self.z,self.w,self.h,self.d) members = [] for o in self.rndr: members.append('render: '+o.save_format()) for o in self.cuto: members.append('cutout: '+o.save_format()) return s + '/\n '.join(members) + '>' #----------------------CAMERA CLASS-------------------------- class Perspective_Camera(object): def __init__(self, x,y,z, azimuth,inclination, fov, render_objects, cutout_objects): if verbose: print("making PerspectiveCamera: pos=({0},{1},{2}) azimuth={3} inclination={4} field of view={5} render objects={6} cutout objects={7}".format(x,y,z, azimuth, inclination, fov, render_objects, cutout_objects)) self.x, self.y, self.z = x,y,z self.a, self.i = azimuth, inclination self.fov = fov self.rndr = render_objects self.cuto = cutout_objects def forward(self): a, i = self.a, self.i + pi/2 return (sin(i)*sin(a)), -(cos(i)), (sin(i)*cos(a)) def right(self): a, i = self.a, self.i + pi/2 return -(sin(i)*cos(a)), 0, (sin(i)*sin(a)) def up(self): a, i = self.a, self.i + pi/2 return 0,0.8,0 def look_at(self, x,y,z): x = x-self.x y = y-self.y z = z-self.z if y == 0: y = 0.001 self.a = atan(x/z) if x == 0: x = 0.001 r = sqrt(x**2 + y**2 + z**2) self.i = acos(y/r) def cast_ray(self, right,up, max_dist, depth_resolution): if verbose: profiler("calculating", False) a, i = self.a + right*pi/self.fov, self.i - (up/self.fov)*pi + pi/2 if extra_verbose: print("Raycast started for pixel",x,y,"with angle a:",a,", i:",i) vx,vy,vz = (sin(i)*sin(a)), -(cos(i)), (sin(i)*cos(a)) if verbose: profiler("calculating") for dist in range(min_dist,max_dist,depth_resolution): if verbose: profiler("calculating", False) ray_pos = self.x + vx*dist, self.y + vy*dist, self.z + vz*dist if super_verbose: print("Ray test dist",dist,"in: ", round(ray_pos[0]),round(ray_pos[1]),round(ray_pos[2]),"with a:", round(a,4), " i:",round(i,4)) if verbose: profiler("calculating") hit_cuto = False try: if so != pc and so.collidepoint(ray_pos[0],ray_pos[1],ray_pos[2]): if use_domains: dmn = (int(round((right+0.5)*rx)),int(round((up+0.5)*ry))) domains[dmn] = o if verbose: profiler("calculating",False) c = (1-(dist-min_dist)/float(max_dist-min_dist))*255 if verbose: profiler("calculating") if extra_verbose: print("Ray hit selected object in: ", round(ray_pos[0]),round(ray_pos[1]),round(ray_pos[2]),"with a:", round(a,4), " i:",round(i,4)) return (c,c,c) except Exception as e: if so != pc: raise e for o in self.cuto: if o.collidepoint(ray_pos[0],ray_pos[1],ray_pos[2]): hit_cuto = True if extra_verbose: print("Ray hit cutout in: ", round(ray_pos[0]),round(ray_pos[1]),round(ray_pos[2]),"with a:", round(a,4), " i:",round(i,4)) break if hit_cuto: continue for o in self.rndr: if o.collidepoint(ray_pos[0],ray_pos[1],ray_pos[2]): if use_domains: dmn = (int(round((right+0.5)*rx)),int(round((up+0.5)*ry))) domains[dmn] = o r,g,b = o.collidepoint(ray_pos[0],ray_pos[1],ray_pos[2]) if verbose: profiler("calculating",False) d = (1-(dist-min_dist)/float(max_dist-min_dist)) c = d*r,d*g,d*b if verbose: profiler("calculating") if extra_verbose: print("Ray hit render object in: ", round(ray_pos[0]),round(ray_pos[1]),round(ray_pos[2]),"with a:", round(a,4), " i:",round(i,4)) return c if extra_verbose: print("Ray failed to hit anything in: ", round(ray_pos[0]),round(ray_pos[1]),round(ray_pos[2]),"with a:", round(a,4), " i:",round(i,4)) return -1 def save_format(self): return 'camera: {0},{1},{2}, {3},{4}, {5}'.format( round(self.x,4),round(self.y,4),round(self.z,4),round(self.a,4),round(self.i,4),round(self.fov,4)) #Is v between i1 and i2? def between(v, i1, i2): if i2 < i1: i1, i2 = i2, i1 if i1 <= v and v < i2: return True return False #----------------------RENDERING CLASSES END-------------------------- #-------------------THREADING----------------------- render_wakeup_event = threading.Event() screen_lock = threading.Lock() class RenderThread(threading.Thread): def run(self): while running: if(cast_rays==[]): render_wakeup_event.wait() render_wakeup_event.clear() continue try: x,y,d = cast_rays.pop(0) except Exception as e: print(e) continue if verbose: profiler("rendering", False) if(d == -1): d = (0,0,0)#(100,125,200) if blur: hs = scale//2 x = x*scale y = y*scale for i in range(-1,2): for j in range(-1,2): if i == 0 and j == 0: c = d else: try: c = screen.get_at((x+scale*i,y+scale*j)) except: if verbose: profiler("rendering") continue if c != d: if blur_on_black or c != (0,0,0): if c == (0,0,0): c = blur_color(c,d,blur_on_black) else: c = blur_color(d,c,0.5) screen_lock.acquire() screen.fill(c, (x+hs*i, y+hs*j,hs,hs)) screen_lock.release() if extra_verbose and c != (0,0,0): print("rendered:", c, "at", (x+hs*i, y+hs*j)) else: screen_lock.acquire() screen.fill(d, (scale*x, scale*y,scale,scale)) screen_lock.release() if extra_verbose and d != (0,0,0): print("rendered:", d, "at", (scale*x, scale*y)) if verbose: profiler("rendering") cast_rays = [] raycast_wakeup_event = threading.Event() raycast_pop_lock = threading.Lock() class RaycastThread(threading.Thread): def run(self): while running: if(unrenewed_pixels==[]): raycast_wakeup_event.clear() raycast_wakeup_event.wait() continue raycast_pop_lock.acquire() if unrenewed_pixels == []: raycast_pop_lock.release() continue x,y = unrenewed_pixels.pop(random.randint(0,len(unrenewed_pixels)-1)) raycast_pop_lock.release() if verbose: profiler("casting rays", False) d = pc.cast_ray(x/float(rx/3.0*2)-0.5, y/float(rx)-0.5, max_dist, depth_resolution) if verbose: profiler("casting rays") cast_rays.append((x,y,d)) render_wakeup_event.set() #---------------SAVING AND LOADING------------------ def format_settings_to_string(): t = [] for s in settings: if s == 'Field of view' or get_setting_value(s) == None: continue t.append(s + '\t' + str(get_setting_value(s))) return 'settings: ' + ', '.join(t) def load_settings_from_string(input_text): t = input_text.strip()[9:].split(',') for s in t: k, v = s.split('\t') set_setting_value(k.strip(), v) def save_objects_to_file(): f = open(scene_file,"w") f.write(pc.save_format() + ';\n') if verbose: print(pc.save_format()) f.write(format_settings_to_string() + ';\n') if verbose: print(format_settings_to_string()) for o in pc.rndr: f.write('render: '+o.save_format()+';\n') if verbose: print('render: '+o.save_format()+';') for o in pc.cuto: f.write('cutout: '+o.save_format()+';\n') if verbose: print('cutout: '+o.save_format()+';') def load_objects_from_file(): file = None try: file = open(scene_file,'r') load_objects_from_string(file.read()) except: return def load_default_scene(): load_objects_from_string('''camera: -75.9,325.94,-116.06, 0.53,-0.55, 3.8; settings: Ray step length 5, Maximum distance 700, Minimum distance 250, Blur False, Blur on black 0.5, Resolution width 80, Resolution height 60, Resolution scale 10, Hide debug True; render: group: -100,-200,-100, 1,1,1 ; render: sphere: 350,210,85, 5, 255,255,0; render: sphere: 300,230,120, 5, 255,255,0; render: sphere: 330,135,65, 5, 255,255,0; render: sphere: 340,170,100, 5, 255,255,0; render: sphere: 310,140,130, 5, 255,255,0; render: sphere: 75,210,350, 5, 255,255,0; render: sphere: 65,135,330, 5, 255,255,0; render: sphere: 90,170,340, 5, 255,255,0; render: sphere: 130,140,310, 5, 255,255,0; render: group: 0,-7,0, 1,1,1 ; render: group: 258,190,90, 1,1,1 ; render: group: 90,190,258, 1,1,1 ; render: group: 155,205,320, 1,1,1 ; render: group: 110,133,140, 1,1,1 ; render: cylinder: 110,133,130, -6,60,1, 160,80,80; render: sphere: 100,110,220, 50, 100,100,255; render: sphere: 220,120,100, 50, 100,255,100; render: sphere: 200,190,200, 50, 255,100,100;''') def load_objects_from_string(s): global pc for l in s.split(';'): l = l.strip(' \r\n') if l.strip()[:7] == 'render:': l = l[7:].strip() add_render_object(make_object_from_string(l)) elif l.strip()[:7] == 'cutout:': l = l[7:].strip() add_hole_object(make_object_from_string(l)) elif l.strip()[:7] == 'camera:': pc = make_camera_from_string(l) all_objects.append(pc) elif l.strip()[:9] == 'settings:': load_settings_from_string(l) def make_object_from_string(input_text): if input_text.strip()[:5] == "cube:": return make_cube_from_string(input_text.strip()[5:].strip()) elif input_text.strip()[:7] == "sphere:": return make_sphere_from_string(input_text.strip()[7:].strip()) elif input_text.strip()[:9] == "cylinder:": return make_cylinder_from_string(input_text.strip()[9:].strip()) elif input_text.strip()[:6] == "group:": return make_group_from_string(input_text.strip()[6:].strip()) else: raise Exception("erronous input " + input_text) def make_group_from_string(input_text): group_specs, members = input_text.split('<') specs = [] for i in group_specs.split(','): specs.append(int(i)) new_group = ObjectGroup(specs[0],specs[1],specs[2], specs[3],specs[4],specs[5], [], []) members = members.strip(' >').split('/') for l in members: if l.strip()[:7] == 'render:': l = l.strip()[7:].strip() new_group.rndr.append(make_object_from_string(l)) elif l.strip()[:7] == 'cutout:': l = l.strip()[7:].strip() new_group.cuto.append(make_object_from_string(l)) else: print('Strange group member: ' + l) return new_group def make_sphere_from_string(input_text): s = input_text.split(',') specs = [] for spec in s: try: specs.append(int(spec.strip())) except Exception as e: print(input_text) raise e if len(specs) == 4: return Sphere(specs[0],specs[1],specs[2],specs[3]) elif len(specs) == 7: return Sphere(specs[0],specs[1],specs[2],specs[3],(specs[4],specs[5],specs[6])) else: raise Exception("invalid number of inputs in "+input_text+" -> ",len(specs)) def make_cylinder_from_string(input_text): s = input_text.split(',') specs = [] for spec in s: try: specs.append(int(spec.strip())) except Exception as e: print(input_text) raise e if len(specs) == 6: return Cylinder(specs[0],specs[1],specs[2],specs[3],specs[4], specs[5]) elif len(specs) == 9: return Cylinder(specs[0],specs[1],specs[2],specs[3],specs[4], specs[5], (specs[6],specs[7],specs[8])) else: raise Exception("invalid number of inputs in "+input_text+" -> ",len(specs)) def make_cube_from_string(input_text): s = input_text.split(',') specs = [] for spec in s: try: specs.append(int(spec.strip())) except Exception as e: print(input_text) raise e if len(specs) == 6: return Cube(specs[0],specs[1],specs[2],specs[3],specs[4],specs[5]) elif len(specs) == 9: return Cube(specs[0],specs[1],specs[2],specs[3],specs[4],specs[5],(specs[6],specs[7],specs[8])) else: raise Exception("invalid number of inputs in "+input_text,len(specs)) def make_camera_from_string(input_text): s = input_text.strip()[7:].split(',') specs = [] for spec in s: try: specs.append(float(spec.strip())) except Exception as e: print(s) raise e if len(specs) == 6: return Perspective_Camera(specs[0],specs[1],specs[2],specs[3],specs[4],specs[5], [], []) else: raise Exception("invalid number of inputs in "+input_text,len(specs)) def add_render_object(o): pc.rndr.append(o) all_objects.append(o) def add_hole_object(o): pc.cuto.append(o) all_objects.append(o) #-----------------SAVING AND LOADING END------------------ def shut_down(): global running, unrenewed_pixels, cast_rays running = False unrenewed_pixels = [] raycast_wakeup_event.set() cast_rays = [] render_wakeup_event.set() def reset_screen(wipe_black = False): 'Refresh screen' global unrenewed_pixels, frame_counter, render_timer, cast_rays if verbose: print("Refresh requested") frame_counter = 0 render_timer = pygame.time.get_ticks() unrenewed_pixels=positions[:] raycast_wakeup_event.set() cast_rays = [] if wipe_black: screen.fill((0,0,0)) def blur_color(c1,c2,ratio): 'Mix two colors in specified ratio. Ratio: 1 -> c1, 0 -> c2' if ratio <= 0: return c2 elif ratio >= 1: return c1 else: return c1[0]*ratio + c2[0]*(1-ratio), c1[1]*ratio + c2[1]*(1-ratio), c1[2]*ratio + c2[2]*(1-ratio) profiler_table = {} def profiler(event, close = True): try: profiler_table[event] += (-1+2*close)*pygame.time.get_ticks() except: profiler_table[event] = (-1+2*close)*pygame.time.get_ticks() #--------------------SETTINGS MENU--------------------------- def clamp(value, lower_limit, upper_limit): if value > upper_limit: return upper_limit elif value < lower_limit: return lower_limit return value def clamp_raise(value, addition, lower_limit, upper_limit): return clamp(value+addition, lower_limit, upper_limit) def change_setting(setting, value): global resolution, blur, resolution_changed,depth_resolution,\ max_dist,min_dist,blur_on_black,rx,ry,resolution,scale,\ hide_debug if setting == 'Ray step length': depth_resolution = clamp_raise(depth_resolution, 1*(-1+2*value), 1, 999) elif setting == 'Maximum distance': max_dist = clamp_raise(max_dist, 10*(-1+2*value), -99999, 99999) elif setting == 'Minimum distance': min_dist = clamp_raise(min_dist, 10*(-1+2*value), -99999, 99999) elif setting == 'Field of view': pc.fov = clamp_raise(pc.fov, 0.1*(-1+2*value), -20, 20) elif setting == 'Blur': blur = value if blur and scale%2: scale += 1 resolution_changed = True elif setting == 'Blur on black': blur_on_black = clamp_raise(blur_on_black, 0.01*(-1+2*value), 0, 1) elif setting == 'Resolution width': rx = clamp_raise(rx, 2*((-1+2*value)), 8, 1920//scale) resolution = rx, ry resolution_changed = True elif setting == 'Resolution height': ry = clamp_raise(ry, 2*((-1+2*value)), 8, 1080//scale) resolution = rx, ry resolution_changed = True elif setting == 'Resolution scale': scale = clamp_raise(scale, (1+blur-blur*scale%2)*((-1+2*value)), 1, min(1920//rx,1080//ry)) resolution_changed = True elif setting == 'Hide debug': hide_debug = value elif setting == 'Redraw with current settings': if resolution_changed: resolution_changed = False update_resolution() reset_screen(True) def update_resolution(): global screen prepare_positions() screen_lock.acquire() screen = pygame.display.set_mode((rx*scale, ry*scale)) screen_lock.release() def get_setting_value(setting): if setting == 'Ray step length': return depth_resolution elif setting == 'Maximum distance': return max_dist elif setting == 'Minimum distance': return min_dist elif setting == 'Field of view': return round(pc.fov,1) elif setting == 'Blur': return blur elif setting == 'Blur on black': return round(blur_on_black,2) elif setting == 'Resolution width': return rx elif setting == 'Resolution height': return ry elif setting == 'Resolution scale': return scale elif setting == 'Hide debug': return hide_debug def set_setting_value(setting, value): global resolution, blur, resolution_changed,depth_resolution,\ max_dist,min_dist,blur_on_black,rx,ry,resolution,scale,\ hide_debug if setting == 'Ray step length': depth_resolution = int(value) elif setting == 'Maximum distance': max_dist = int(value) elif setting == 'Minimum distance': min_dist = int(value) elif setting == 'Blur': blur = (value == "True") elif setting == 'Blur on black': blur_on_black = float(value) elif setting == 'Resolution width': rx = int(value) resolution = rx, ry elif setting == 'Resolution height': ry = int(value) resolution = rx, ry elif setting == 'Resolution scale': scale = int(value) elif setting == 'Hide debug': hide_debug = (value == 'True') else: if not setting in settings or get_setting_value(settings) != None: print('Unknown setting',settings,'with value',value) def handle_settings_menu(): global settings_menu_mode, settings_menu_selection, screen, resolution_changed decrease = increase = False for e in pygame.event.get(): if e.type == pygame.QUIT: shut_down() return elif e.type == pygame.KEYDOWN: if e.key == pygame.K_ESCAPE: shut_down() return elif e.key == pygame.K_DOWN: settings_menu_selection += 1 if settings_menu_selection >= len(settings): settings_menu_selection = 0 elif e.key == pygame.K_UP: settings_menu_selection -= 1 if settings_menu_selection < 0: settings_menu_selection = len(settings)-1 elif e.key == pygame.K_LEFT: decrease = True elif e.key == pygame.K_RIGHT: increase = True elif e.key == pygame.K_RETURN: settings_menu_mode = False if resolution_changed: resolution_changed = False update_resolution() reset_screen() return elif e.key == pygame.K_BACKSPACE: settings_menu_mode = False resolution_changed = False return fly_camera(pygame.key.get_pressed()) if increase: change_setting(settings[settings_menu_selection],True) reset_screen() elif decrease: change_setting(settings[settings_menu_selection],False) reset_screen() i = 65 p = dbg_font.render('->',True,(240,240,240)) screen_lock.acquire() for s in settings: screen.fill((0,0,0), (0,i-4,300,20)) t = dbg_font.render(s+'',True,(240,240,240)) screen.blit(t, (20,i)) if get_setting_value(s) != None: v = dbg_font.render('- '+str(get_setting_value(s))+' +', True, (240,240,240)) screen.blit(v, (295-v.get_width(),i)) if (i-65)/20 == settings_menu_selection: screen.blit(p, (1,i)) i += 20 pygame.display.flip() screen_lock.release() pygame.time.wait(frame_start + frame_time - pygame.time.get_ticks()) #----------------------CAMERA CONTROL-------------------- def add_vector3(v1, v2): return v1[0]+v2[0],v1[1]+v2[1],v1[2]+v2[2] def sub_vector3(v1, v2): 'v1 - v2' return v1[0]-v2[0],v1[1]-v2[1],v1[2]-v2[2] def fly_camera(pressed_keys): #------------------NO CLIP FLYING------------------------ f = (0,0,0) if pressed_keys[pygame.K_w]: f = add_vector3(f, pc.forward()) if pressed_keys[pygame.K_s]: f = sub_vector3(f, pc.forward()) if pressed_keys[pygame.K_a]: f = add_vector3(f, pc.right()) if pressed_keys[pygame.K_d]: f = sub_vector3(f, pc.right()) if pressed_keys[pygame.K_q]: f = add_vector3(f, pc.up()) if pressed_keys[pygame.K_z]: f = sub_vector3(f, pc.up()) if f != (0,0,0): pc.x += f[0]*fly_speed pc.y += f[1]*fly_speed pc.z += f[2]*fly_speed reset_screen() #---------------------INITIALIZATION--------------------- instructions = ['hold down h for help', 'move selected: up,down,left,right,i,k', 'toggle selected object hole/cube: np5', 'resize selected object: numpad', 'add object: c', 'select object: numbers/+/-', 'redraw: e; camera location reset: r', 'aim camera: up/down/left/right', 'fly: w/a/s/d/q/z'] settings = ['Ray step length', 'Maximum distance', 'Minimum distance', 'Field of view', 'Blur', 'Blur on black', 'Resolution width', 'Resolution height', 'Resolution scale', 'Hide debug', 'Redraw with current settings'] #Debug variables render_time = 0 frame_start = -1 frame_counter = 0 render_timer = pygame.time.get_ticks() verbose = extra_verbose = super_verbose = False if '-v' in sys.argv: verbose = True elif '-vv' in sys.argv: verbose = extra_verbose = True elif '-vvv' in sys.argv: verbose = extra_verbose = super_verbose = True #super_verbose = extra_verbose = verbose = True use_domains = False if '--domains' in sys.argv: use_domains = True hide_debug = True if '--debug' in sys.argv: hide_debug = False resolution_changed = False #Input variables input_text = '' running = True pressed_key = None return_pressed = False backspace_pressed = False input_mode = False cube_add_mode = False hole_add_mode = False settings_menu_mode = False settings_menu_selection = 0 mouse_follow_effect = False #--------------Setting variables-------------- resolution = rx, ry = (40,30) #raycast resolution (huge performance hit) scale = 24 #scale output (no performance hit) depth_resolution = 5 #percision of individual rays (great performance hit) #No inputs or refreshs during rendering just_render = '-r' in sys.argv #furthest distanec to cast to (mild performance hit) max_dist = 550 #furthest distanec to cast to (mild performance hit) min_dist = 100 #blur when scaled output (small performance hit) blur = False #blend balance with bg color when blur is on blur_on_black = 0.5 #file to load and save the scene scene_file = "objects.scene" #target fps (no big difference) target_fps = 40 frame_time = 1000//target_fps #could improve performance of heavily threaded systems number_of_raycast_threads = 20 #a single render thread should be enough for everybody number_of_RenderThreads = 1 #speeds fly_speed = 5 block_move_speed = 1 block_resize_speed = 1 #-------------------LOAD SAVE FILE-------------------- pc = None #Perspective Camera all_objects = [] load_objects_from_file() #Load saved data #In case of no saved data, create some if pc == None: load_default_scene() #Make camera the selected object so = pc if all_objects == []: all_objects = [pc] + pc.rndr + pc.cuto #Prepare positions def prepare_positions(): global positions, domains positions = [] if use_domains: domains = {} for y in range(ry): for x in range(rx): positions.append((x,y)) if use_domains: domains[(x,y)] = None prepare_positions() unrenewed_pixels = positions[:] #Copy positions to a refresh list #Initializing pygame, key repeat, screen, font and RenderThread pygame.init() pygame.key.set_repeat(300,100) screen = pygame.display.set_mode((scale*rx, scale*ry)) dbg_font = pygame.font.Font(pygame.font.get_default_font(), 14) #------------MAKE SOME RAYCAST AND RENDER THREADS------------- RenderThreads = [] for i in range(number_of_RenderThreads): RenderThreads.append(RenderThread()) RenderThreads[-1].start() RaycastThreads = [] for i in range(number_of_raycast_threads): RaycastThreads.append(RaycastThread()) RaycastThreads[-1].start() #-----------------------MAIN LOOP---------------------------- if verbose: profiler("running", False) if verbose: print("main loop starts") while running: frame_start = pygame.time.get_ticks() if just_render: for e in pygame.event.get(): if e.type == pygame.QUIT: running = False pygame.time.wait(frame_time - (pygame.time.get_ticks() - frame_start)) pygame.display.flip() continue if settings_menu_mode: handle_settings_menu() continue #---------------THE BOOK OF INPUT---------------------- for e in pygame.event.get(): if e.type == pygame.QUIT: shut_down() elif e.type == pygame.KEYDOWN: if verbose: print("key pressed:",pygame.key.name(e.key)) if not e.key in (pygame.K_h, pygame.K_ESCAPE, pygame.K_b, pygame.K_g, pygame.K_t, pygame.K_e, pygame.K_x) \ and not between(e.key, pygame.K_0, pygame.K_9+1): reset_screen() if e.key == pygame.K_ESCAPE: shut_down() elif input_mode and e.key in (pygame.K_KP_MINUS, pygame.K_MINUS, 47): pressed_key = '-' elif e.key in (pygame.K_PLUS, pygame.K_KP_PLUS): try: so = all_objects[all_objects.index(so)+1] except: so = all_objects[0] reset_screen() elif e.key in (e.key == pygame.K_KP_MINUS, pygame.K_MINUS): try: so = all_objects[all_objects.index(so)-1] except: so = all_objects[len(all_objects)-1] reset_screen() elif e.key >= pygame.K_0 and e.key <= pygame.K_9: pressed_key = e.key - pygame.K_0 elif input_mode and e.key >= pygame.K_KP0 and e.key <= pygame.K_KP9: pressed_key = e.key - pygame.K_KP0 elif e.key in (pygame.K_RETURN,pygame.K_KP_ENTER): return_pressed = True elif e.key == pygame.K_BACKSPACE: backspace_pressed = True elif e.key in (pygame.K_KP_PERIOD,pygame.K_PERIOD,pygame.K_COMMA): pressed_key = ',' elif e.key == pygame.K_KP5: if so in pc.rndr: pc.rndr.remove(so) pc.cuto.append(so) elif so in pc.cuto: pc.cuto.remove(so) pc.rndr.append(so) elif e.key == pygame.K_o: settings_menu_mode = True continue if e.type == pygame.KEYUP and e.key in (pygame.K_h,None): #redraw after help reset_screen() pressed_keys = pygame.key.get_pressed() mx,my = pygame.mouse.get_rel() #------------------INPUT OBJECTS SPECS------------------------------ if input_mode: if return_pressed: input_mode = False return_pressed = False elif backspace_pressed: input_text = input_text[:-1] backspace_pressed = False elif pressed_key != None: input_text += str(pressed_key) pressed_key = None info_text = '' if cube_add_mode: info_text = "Make cube: x,y,z, w,h,d [, color_r, _g, _b]" elif hole_add_mode: info_text = "Make hole: x,y,z, w,h,d" screen_lock.acquire() debug_text = dbg_font.render(info_text, True, (220,100,100)) screen.fill((0,0,0,0.5),(0,130,280,20)) screen.blit(debug_text, (5,135)) debug_text = dbg_font.render(input_text, True, (220,100,100)) screen.fill((0,0,0,0.5),(0,150,280,20)) screen.blit(debug_text, (5,155)) pygame.display.flip() screen_lock.release() continue #----------------------ADDING OBJECTS----------------------- if pressed_keys[pygame.K_c]: input_mode = True cube_add_mode = True pressed_key = None input_text = '' if pressed_keys[pygame.K_v]: input_mode = True hole_add_mode = True pressed_key = None input_text = '' if not input_mode and (cube_add_mode or hole_add_mode): if input_text == '': cube_add_mode = hole_add_mode = False elif cube_add_mode: cube_add_mode = False add_render_object(make_object_from_string('cube:'+input_text)) else: hole_add_mode = False add_hole_object(make_cube_from_string('cube:'+input_text)) input_text = '' reset_screen() #----------------------MOUSE LOOK------------------------ if pygame.mouse.get_pressed()[0] and mx != 0 and my != 0: pc.a -= mx/500 pc.i += my/500 reset_screen() if pressed_keys[pygame.K_f]: pc.look_at(0,0,0) reset_screen() fly_camera(pressed_keys) #------------------MOVING OBJECTS--------------------- if so != pc: if pressed_keys[pygame.K_UP]: so.z += block_move_speed reset_screen() if pressed_keys[pygame.K_DOWN]: so.z -= block_move_speed reset_screen() if pressed_keys[pygame.K_LEFT]: so.x -= block_move_speed reset_screen() if pressed_keys[pygame.K_RIGHT]: so.x += block_move_speed reset_screen() if pressed_keys[pygame.K_i]: so.y += block_move_speed reset_screen() if pressed_keys[pygame.K_k]: so.y -= block_move_speed reset_screen() #-------------------RESIZING OBJECTS----------------- if pressed_keys[pygame.K_KP8]: so.h -= block_resize_speed reset_screen() if pressed_keys[pygame.K_KP2]: so.h += block_resize_speed reset_screen() if pressed_keys[pygame.K_KP4]: so.w -= block_resize_speed reset_screen() if pressed_keys[pygame.K_KP6]: so.w += block_resize_speed reset_screen() if pressed_keys[pygame.K_KP7]: so.d += block_resize_speed reset_screen() if pressed_keys[pygame.K_KP3]: so.d -= block_resize_speed reset_screen() else: #-------------------ROTATING CAMERA----------------- if pressed_keys[pygame.K_UP]: pc.i += 0.01 reset_screen() if pressed_keys[pygame.K_DOWN]: pc.i -= 0.01 reset_screen() if pressed_keys[pygame.K_LEFT]: pc.a -= 0.01 reset_screen() if pressed_keys[pygame.K_RIGHT]: pc.a += 0.01 reset_screen() #-----------------------X-EFFECT--------------------------- if pressed_keys[pygame.K_x]: if not mouse_follow_effect: #screen.fill((0,0,0)) pass mouse_follow_effect = True ex,ey = pygame.mouse.get_pos() effect_radius = 10 effect_pos = [ex//scale,ey//scale] ex,ey = effect_pos for i in range(effect_pos[0]-effect_radius,effect_pos[0]+effect_radius): for j in range(effect_pos[1]-effect_radius,effect_pos[1]+effect_radius): if not (i,j) in unrenewed_pixels: unrenewed_pixels.append((i,j)) raycast_wakeup_event.set() else: mouse_follow_effect = False #----------------------PANIC RESET CAMERA------------------------ if pressed_keys[pygame.K_r]: pc = Perspective_Camera(26,450,36, 0.528,-1.146, 3.0, pc.rndr, pc.cuto) so = pc reset_screen() #--------------------SELECT OBJECT---------------------- if type(pressed_key) == int: try: if use_domains: for k in domains: v = domains[k] if v == so or v == all_objects[pressed_key]: if super_verbose: print("Domains[",k,"]=",v) unrenewed_pixels.append(k) raycast_wakeup_event.set() so = all_objects[pressed_key] pressed_key = None else: so = all_objects[pressed_key] reset_screen() except: so = pc reset_screen() #----------------REMOVING SELECTED OBJECT------------------ if so != pc and so in all_objects and pressed_keys[pygame.K_DELETE]: all_objects.remove(so) try: pc.rndr.remove(so) except: pc.cuto.remove(so) so = pc reset_screen() #-----------------END OF THE BOOK OF INPUT-------------------- #--------------RENDER DEBUG AND INSTRUCTIONS------------------ if(unrenewed_pixels!=[]): frame_counter += 1 render_time = pygame.time.get_ticks()-render_timer if not hide_debug: rendered = len(positions)-len(unrenewed_pixels) debug_string = 'step:{0} fov:{1} range:{6}..{2} d/s:{5} time:{4}ms frame:{3}'.format(depth_resolution, round(pc.fov,2), max_dist,frame_counter, render_time, round(rendered*1000/(render_time+1)), min_dist) debug_text1 = dbg_font.render(debug_string, True, (220,100,100)) if type(so) in (Cube,ObjectGroup): debug_string = 'blur:{6} bleed:{7} pos:({0},{1},{2}) size:({3},{4},{5})'.format(round(so.x),round(so.y),round(so.z), round(so.w),round(so.h),round(so.d), int(blur), round(blur_on_black,2)) elif type(so) == Sphere: debug_string = 'blur:{4} bleed:{5} pos:({0},{1},{2}) radius:{3}'.format(round(so.x),round(so.y),round(so.z), round(so.r), int(blur), round(blur_on_black,2)) elif type(so) == Cylinder: debug_string = 'blur:{5} bleed:{6} pos:({0},{1},{2}) r:{3} l:{4}'.format(round(so.x),round(so.y),round(so.z), round(so.r),round(so.l), int(blur), round(blur_on_black,2)) else: debug_string = 'blur:{5} bleed:{6} pos:({0},{1},{2}) angle:{3},{4}'.format(round(so.x),round(so.y),round(so.z), round(pc.a,4), round(pc.i,4), int(blur), round(blur_on_black,2)) debug_text2 = dbg_font.render(debug_string, True, (220,100,100)) if not pressed_keys[pygame.K_h]: screen.fill((0,0,0),(0,scale*ry-20,162,20)) screen.blit(dbg_font.render(instructions[0], True, (220,100,100)),(5,scale*ry-17)) screen_lock.acquire() screen.blit(debug_text1, (5,0)) screen.blit(debug_text2, (5,20)) if pressed_keys[pygame.K_h]: screen.fill((0,0,0),(0,scale*ry-20*len(instructions)+20,355,20*len(instructions))) for i in range(1,len(instructions)): screen.blit(dbg_font.render(instructions[i], True, (220,100,100)),(5,scale*ry-20*i)) pygame.display.flip() if not hide_debug: screen_lock.release() screen.fill((0,0,0),(0,0,450,20)) screen.fill((0,0,0),(0,20,380,20)) if extra_verbose: print("display flipped") #----------------------REFRESH------------------------ #Refresh as "early" in the frame as possible to give #renderers and raycasters time to draw as many dots as #possible before flipping and wiping them. if pressed_keys[pygame.K_e]: reset_screen(True) #Sleep until the frame time is complete if verbose: profiler("idling", False) pygame.time.wait(frame_time - (pygame.time.get_ticks() - frame_start)) if verbose: profiler("idling") #------------------MAIN LOOP END---------------------- #Save if not just_render: save_objects_to_file() if verbose: profiler("running") pygame.quit() #Print profiler data if verbose: for k in profiler_table: print("time took", k,profiler_table[k],"ms")