Недавно я написал простую программу для проецирования точек в трехмерном пространстве на экран и создал класс полигонов, чтобы продемонстрировать это. И, кроме пары визуальных глюков, я вполне доволен этим.
Он работает, вычисляя разницу в углах каждой точки относительно камеры и угла, на который смотрит камера. Это делается в горизонтальной и вертикальной плоскостях отдельно. При расчете расстояния по горизонтали от камеры до точки (которая используется для вычисления вертикального угла) я умножаю на cos(abs(self.angle[0] - self.camera.angle[0]))
расчетное проекционное расстояние, а не евклидово расстояние.
Однако естьодна вопиющая проблема, которая заключается в том, что при повороте камеры вверх или вниз она вместо этого перемещает весь экран вверх или вниз, не изменяя перспективу. Это означает, что все выглядит так, как должно, только когда камера идеально выровнена. В противном случае, все немного искажается (это лучше всего увидеть, поднявшись и опустив взгляд).
Вот изображение многоугольника сбоку (выглядит нормально), а затем видно прямо сверху, смотря вниз (очень искажено). Это должно быть так, как если бы оно просматривалось прямо, но это не то, что происходит.
Нормальный: Искаженный:
Я уже несколько часов пытаюсь выяснить, что я могу сделать, чтобы решить эту проблему, но на самом деле не уверен, и я хотел бы помочь. Я включил свой код ниже, там не так много работы, поэтому я надеюсь, что вы сможете понять его.
Вы можете перемещаться с помощью WASD и вращать камеру с помощью клавиш со стрелками. Пробел и LSHIFT перемещаются вверх и вниз соответственно.
import sys
import pygame
import math
from pygame.locals import *
pygame.init()
WIDTH = 800
HEIGHT = 600
RATIO = WIDTH / HEIGHT
SCREEN = pygame.display.set_mode((WIDTH,HEIGHT))
CLOCK = pygame.time.Clock()
FPS = 60
pygame.display.set_caption('3D Test')
# #------Colours------# #
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
CYAN = (0, 255, 255)
DARKGREEN = (0, 64, 0)
DARKGREY = (64, 64, 64)
GREY = (128, 128, 128)
GREEN = (0, 128, 0)
LIME = (0, 255, 0)
MAGENTA = (255, 0, 255)
MAROON = (128, 0, 0)
NAVYBLUE = (0, 0, 128)
OLIVE = (128, 128, 0)
PURPLE = (128, 0, 128)
RED = (255, 0, 0)
SILVER = (192, 192, 192)
TEAL = (0, 128, 128)
WHITE = (255, 255, 255)
YELLOW = (255, 255, 0)
# #-------------------# #
class Camera:
def __init__(self):
self.position = [0, 5, -20]
self.angle = [90, -15]
self.fov = 45
self.move_speed = 0.2
self.look_speed = 2
def look_around(self):
keys = pygame.key.get_pressed()
if keys[K_RIGHT]:
self.angle[0] -= self.look_speed
if keys[K_LEFT]:
self.angle[0] += self.look_speed
if keys[K_UP]:
self.angle[1] += self.look_speed
if keys[K_DOWN]:
self.angle[1] -= self.look_speed
if self.angle[0] < -180:
self.angle[0] += 360
elif self.angle[0] > 180:
self.angle[0] -= 360
self.angle[1] = min(max(self.angle[1], -90), 90)
def move(self):
keys = pygame.key.get_pressed()
if keys[K_w]: # Move Forwards
self.position[0] += math.cos(math.radians(self.angle[0])) * self.move_speed
self.position[2] += math.sin(math.radians(self.angle[0])) * self.move_speed
if keys[K_s]: # Move Backwards
self.position[0] -= math.cos(math.radians(self.angle[0])) * self.move_speed
self.position[2] -= math.sin(math.radians(self.angle[0])) * self.move_speed
if keys[K_a]: # Move Left
self.position[2] += math.cos(math.radians(self.angle[0])) * self.move_speed
self.position[0] -= math.sin(math.radians(self.angle[0])) * self.move_speed
if keys[K_d]: # Move Right
self.position[2] -= math.cos(math.radians(self.angle[0])) * self.move_speed
self.position[0] += math.sin(math.radians(self.angle[0])) * self.move_speed
if keys[K_SPACE]: # Move Up
self.position[1] += self.move_speed
if keys[K_LSHIFT]: # Move Down
self.position[1] -= self.move_speed
def update(self):
self.move()
self.look_around()
class Point:
def __init__(self, camera, position):
self.camera = camera
self.position = position
self.angle = [0, 0]
self.update_angle()
def update_angle(self):
# Get the horizontal angle between the camera and the point
self.angle[0] = math.degrees(math.atan2(self.position[2] - self.camera.position[2],
self.position[0] - self.camera.position[0]))
horizontal_distance = math.hypot(self.position[0] - self.camera.position[0],
self.position[2] - self.camera.position[2])
vertical_distance = self.position[1] - self.camera.position[1]
# Get the vertical angle between the camera and the point
self.angle[1] = math.degrees(math.atan2(vertical_distance,
math.cos(math.radians(abs(self.angle[0] - self.camera.angle[0]))) * horizontal_distance))
def update(self):
self.update_angle()
def get_screen_pos(self):
# Calculate the difference in the angle of the camera and the point
view_angle = [self.camera.angle[0] - self.angle[0],
self.camera.angle[1] - self.angle[1]]
# Calculate the relative position of the point to the camera (on the screen)
relative_position = [view_angle[0] / self.camera.fov,
view_angle[1] / (self.camera.fov / RATIO)]
# Scale the relative position to the size of the screen
position = [int((relative_position[0] + 0.5) * WIDTH), int((relative_position[1] + 0.5) * HEIGHT)]
return position
class Polygon:
def __init__(self, camera, point_list, colour, line_colour, line_width=1):
"""
Creates a polygon out of a list of points
:param camera: Reference to the camera
:param point_list: List of points
:param colour: Fill colour of the polygon
:param line_colour: Line colour of the polygon
:param line_width: With of the edges of the polygon
"""
self.points = [Point(camera, point) for point in point_list]
self.colour = colour
self.line_colour = line_colour
self.line_width = line_width
def update(self):
for point in self.points:
point.update()
def render(self):
screen_points = [point.get_screen_pos() for point in self.points]
pygame.draw.polygon(SCREEN, self.colour, screen_points)
pygame.draw.polygon(SCREEN, self.line_colour, screen_points, self.line_width)
def main():
camera = Camera()
poly = Polygon(camera, [(0, 0, 5), (3, 0, 4), (4, 0, 3), (5, 0, 0), (4, 0, -3), (3, 0, -4), (0, 0, -5), (-3, 0, -4),
(-4, 0, -3), (-5, 0, 0), (-4, 0, 3), (-3, 0, 4)], RED, WHITE, 3)
while True:
SCREEN.fill(BLACK)
events = pygame.event.get()
for event in events:
if event.type == QUIT:
pygame.quit()
sys.exit()
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
pygame.quit()
sys.exit()
camera.update()
poly.update()
poly.render()
pygame.display.update()
CLOCK.tick(FPS)
if __name__ == '__main__':
main()