Basic ray casting with fog.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -99,3 +99,6 @@ ENV/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# Emacs droppings
|
||||
*~
|
||||
|
@@ -1,2 +1,5 @@
|
||||
# Py-Caster
|
||||
* Py-Caster
|
||||
|
||||
Ray Casting implementation with Pygame.
|
||||
|
||||
[[./screenshot.png]]
|
273
py_caster.py
Executable file
273
py_caster.py
Executable file
@@ -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()
|
||||
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
Reference in New Issue
Block a user