From 40c517e25a372e55d168b13a6574a118df128d42 Mon Sep 17 00:00:00 2001 From: Miguel Angel Date: Tue, 31 Oct 2017 00:53:18 -0400 Subject: [PATCH] Basic ray casting with fog. --- .gitignore | 3 + README.md => README.org | 5 +- py_caster.py | 273 ++++++++++++++++++++++++++++++++++++++++ screenshot.png | Bin 0 -> 5260 bytes 4 files changed, 280 insertions(+), 1 deletion(-) rename README.md => README.org (53%) create mode 100755 py_caster.py create mode 100644 screenshot.png diff --git a/.gitignore b/.gitignore index 7bbc71c..d24fd00 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ ENV/ # mypy .mypy_cache/ + +# Emacs droppings +*~ diff --git a/README.md b/README.org similarity index 53% rename from README.md rename to README.org index 5fa48ca..30362e2 100644 --- a/README.md +++ b/README.org @@ -1,2 +1,5 @@ -# Py-Caster +* Py-Caster + Ray Casting implementation with Pygame. + +[[./screenshot.png]] diff --git a/py_caster.py b/py_caster.py new file mode 100755 index 0000000..e32e294 --- /dev/null +++ b/py_caster.py @@ -0,0 +1,273 @@ +#! /usr/bin/env python +import math +import pygame + +############################################################## +# Game parameters +############################################################## + +TITLE = "Py Caster" +FPS = 60 +SCREEN_SIZE = (800, 600) +CEILING_COLOR = (75, 119, 208) +FLOOR_COLOR = (229, 138, 132) +FOG_COLOR = (128, 128, 128) +FOG_NEAR = 1.0 +FOG_FAR = 5.0 + +############################################################## +# Projection parameters +############################################################## + +FB_SIZE = (320, 200) +DEG2RAD = 3.1415926535897932384626433 / 180.0 +FOV = 66.84962236520761 +ANGLE_INCREMENT = FOV / float(FB_SIZE[0]) + +############################################################## +# Player parameters +############################################################## + +PLAYER_TURN_SPEED = 2.0 * DEG2RAD +PLAYER_MOVE_SPEED = 0.1 + +############################################################## +# Vector Classes +############################################################## + +class vec3(object): + def __init__(self, x = 0, y = 0, z = 0): + self.x = x + self.y = y + self.z = z + + def dot(self, v): + return (self.x * v.x) + (self.y * v.y) + (self.z * v.z) + + def length(self): + return math.sqrt(self.dot(self)) + + def normalize(self): + norm = self.length() + if norm > 0: + self.x /= norm + self.y /= norm + self.z /= norm + return self + +class vec2(object): + def __init__(self, x = 0, y = 0): + self.x = x + self.y = y + + def __str__(self): + return "(" + str(self.x) + ", " + str(self.y) + ")" + + def add(self, v): + return vec2(self.x + v.x, self.y + v.y) + + def sub(self, v): + return vec2(self.x - v.x, self.y - v.y) + + def scale(self, k): + return vec2(self.x * k, self.y * k) + + def dot(self, v): + return (self.x * v.x) + (self.y * v.y) + + def length(self): + return math.sqrt(self.dot(self)) + + def distance(self, p): + return p.sub(self).length() + + def normalize(self): + norm = self.length() + if norm > 0: + self.x /= norm + self.y /= norm + return self + + def cross(self, v): + return vec3(0.0, 0.0, (self.x * v.y) - (self.y * v.x)) + +############################################################## +# Ray Class +############################################################## + +class Ray: + def __init__(self, origin = vec2(0.0, 0.0), direction = vec2(0.0, 1.0)): + self.o = origin + self.d = direction.normalize() + + + def __str__(self): + return "Origin: " + str(self.o) + " :: Direction: " + str(self.d) + +############################################################## +# Line Segment Class +############################################################## + +class LineSegment: + def __init__(self, a, b, c = (0, 0, 0)): + self.a = a + self.b = b + self.v = b.sub(a).normalize() + self.color = c + + def intersect(self, r): + def sign(n): + if n == 0: + return 0 + elif n > 0: + return 1 + else: + return -1 + + side = self._classifyPoint2D(r.o) + + v2 = self.b.sub(self.a) if sign(side) > 0 else self.a.sub(self.b) + v3 = vec2(-r.d.y, r.d.x) + + det = v2.dot(v3) + + if abs(det) < 0.00001: + return None + else: + v1 = r.o.sub(self.a) if sign(side) > 0 else self.a.sub(r.o) + t1 = v2.cross(v1).length() / det + t2 = v1.dot(v3) / det + + if t2 >= 0.0 and t2 <= 1.0: + if t1 > 0.0: + return r.o.add(r.d.scale(t1)) + else: + return None + else: + return None + + def _classifyPoint2D(self, point): + v1 = point.sub(self.a) + v2 = vec2(self.v.y, -self.v.x) + return v1.dot(v2) + +############################################################## +# Main Function +############################################################## + +def main(): + # Local variables. + done = False + fog_enabled = False + toggle_fog = False + player_pos = vec2(0.0, 0.0) + player_dir = vec2(-1.0, 0.0) + plane = vec2(0.0, 0.66) + + # Initialize Pygame. + pygame.init() + clock = pygame.time.Clock() + screen = pygame.display.set_mode(SCREEN_SIZE, pygame.HWSURFACE | pygame.DOUBLEBUF) + frame_buffer = pygame.Surface(FB_SIZE, pygame.HWSURFACE) + pygame.mouse.set_visible(False) + pygame.key.set_repeat(17, 17) + + # Define walls. + walls = [LineSegment(vec2(-3.0, 3.0), vec2(3.0, 3.0), (255, 0, 0)), + LineSegment(vec2(3.0, 3.0), vec2(3.0, -3.0), (0, 255, 0)), + LineSegment(vec2(1.5, 1.5), vec2(3.0, 3.0), (255, 255, 0)), + LineSegment(vec2(3.0, -3.0), vec2(-3.0, -3.0), (0, 0, 255)), + LineSegment(vec2(-3.0, -3.0), vec2(-3.0, 3.0), (255, 0, 255))] + + # Main game loop. + try: + while(not done): + fps = clock.get_fps() + 0.001 + pygame.display.set_caption(TITLE + ": " + str(int(fps))) + + # Input capture. + for event in pygame.event.get(): + if (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE) or event.type == pygame.QUIT: + done = True + + if event.type == pygame.KEYDOWN and event.key == pygame.K_UP: + player_pos = player_pos.sub(player_dir.scale(PLAYER_MOVE_SPEED)) + + if event.type == pygame.KEYDOWN and event.key == pygame.K_DOWN: + player_pos = player_pos.add(player_dir.scale(PLAYER_MOVE_SPEED)) + + if event.type == pygame.KEYDOWN and event.key == pygame.K_LEFT: + oldDirX = player_dir.x; + player_dir.x = player_dir.x * math.cos(PLAYER_TURN_SPEED) - player_dir.y * math.sin(PLAYER_TURN_SPEED); + player_dir.y = oldDirX * math.sin(PLAYER_TURN_SPEED) + player_dir.y * math.cos(PLAYER_TURN_SPEED); + oldPlaneX = plane.x; + plane.x = plane.x * math.cos(PLAYER_TURN_SPEED) - plane.y * math.sin(PLAYER_TURN_SPEED); + plane.y = oldPlaneX * math.sin(PLAYER_TURN_SPEED) + plane.y * math.cos(PLAYER_TURN_SPEED); + + if event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT: + oldDirX = player_dir.x; + player_dir.x = player_dir.x * math.cos(-PLAYER_TURN_SPEED) - player_dir.y * math.sin(-PLAYER_TURN_SPEED); + player_dir.y = oldDirX * math.sin(-PLAYER_TURN_SPEED) + player_dir.y * math.cos(-PLAYER_TURN_SPEED); + oldPlaneX = plane.x; + plane.x = plane.x * math.cos(-PLAYER_TURN_SPEED) - plane.y * math.sin(-PLAYER_TURN_SPEED); + plane.y = oldPlaneX * math.sin(-PLAYER_TURN_SPEED) + plane.y * math.cos(-PLAYER_TURN_SPEED); + + if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + if not toggle_fog: + fog_enabled = not fog_enabled + toggle_fog = True + + if event.type == pygame.KEYUP and event.key == pygame.K_SPACE: + if toggle_fog: + toggle_fog = False + + # Render ceiling and floor. + frame_buffer.fill(CEILING_COLOR, pygame.Rect(0, 0, FB_SIZE[0], FB_SIZE[1] / 2)) + frame_buffer.fill(FLOOR_COLOR, pygame.Rect(0, FB_SIZE[1] / 2, FB_SIZE[0], FB_SIZE[1] / 2)) + + # Render walls. + angle = -FOV / 2.0 + for i in xrange(FB_SIZE[0]): + camera_x = 2.0 * (float(i) / float(FB_SIZE[0])) - 1; + r = Ray(vec2(player_pos.x, player_pos.y), vec2(player_dir.x + plane.x * camera_x, player_dir.y + plane.y * camera_x)) + + d = float('Inf') + c = (0, 0, 0) + for l in walls: + p = l.intersect(r) + if p is not None: + _d = player_pos.distance(p) + if _d < d: + d = _d + + def lerp(col, dst): + lt = 0.0 if dst < FOG_NEAR else (1.0 if dst > FOG_FAR else (dst - FOG_NEAR) / (FOG_FAR - FOG_NEAR)) + + red = (FOG_COLOR[0] * lt) + (col[0] * (1.0 - lt)) + green = (FOG_COLOR[1] * lt) + (col[1] * (1.0 - lt)) + blue = (FOG_COLOR[2] * lt) + (col[2] * (1.0 - lt)) + + return (red, green, blue) + + c = lerp(l.color, d) if fog_enabled else l.color + + if d < float('Inf'): + h = int(float(FB_SIZE[1]) / (d * math.cos(angle * DEG2RAD))) + h = FB_SIZE[1] if h > FB_SIZE[1] else h + frame_buffer.fill(c, pygame.Rect(i, -(h / 2) + (FB_SIZE[1] / 2), 1, h)) + + angle += ANGLE_INCREMENT + + pygame.transform.scale(frame_buffer, SCREEN_SIZE, screen) + + pygame.display.update() + clock.tick(FPS) + + except KeyboardInterrupt: + pass + + pygame.quit() + +if __name__ == "__main__": + main() + diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..b59b5a18102353cd7e7effe86ed48812344b4347 GIT binary patch literal 5260 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 literal 0 HcmV?d00001