I can't seem to find the answer to this question anywhere. I realize that you have to use PyOpenGL or something similar to do OpenGL stuff, but I was wondering if its possible to do very basic 3D graphics without any other dependencies.
No, Pygame is a wrapper for SDL, which is a 2D api. Pygame doesn't provide any 3D capability and probably never will.
3D libraries for Python include Panda3D and DirectPython, although they are probably quite complex to use, especially the latter.
Well, if you can do 2d you can always do 3d. All 3d really is is skewed 2 dimensional surfaces giving the impression you're looking at something with depth. The real question is can it do it well, and would you even want to. After browsing the pyGame documentation for a while, it looks like it's just an SDL wrapper. SDL is not intended for 3d programming, so the answer to the real question is, No, and I wouldn't even try.
You can do pseudo-3d games ( like "Doom" ) with pygame only:
http://code.google.com/p/gh0stenstein/
and if you browse the pygame.org site you may find more "3d" games done with python and pygame.
However, if you really want to go into 3d programming you should look into OpenGl, Blender or any other real 3d lib.
Pygame was never originally meant to do 3d, but there is a way you can do 3d with any 2d graphics library. All you need is the following function, which converts 3d points to 2d points, which allows you to make any 3d shape by just drawing lines on a screen.
def convert_to_2d(point=[0,0,0]):
return [point[0]*(point[2]*.3),point[1]*(point[2]*.3)]
This is called pseudo 3d, or 2.5d. This can be done, but may be slow, and is extremely difficult to do, so it is suggested that you use a library meant for 3d.
It does not support, but combining with PyOpenGL you can make use of the power of both, here is a full example
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import random
vertices = ((1, -1, -1),(1, 1, -1),(-1, 1, -1),(-1, -1, -1),(1, -1, 1),(1, 1, 1),(-1, -1, 1),(-1, 1, 1))
edges = ((0,1),(0,3),(0,4),(2,1),(2,3),(2,7),(6,3),(6,4),(6,7),(5,1),(5,4),(5,7))
surfaces = ((0,1,2,3),(3,2,7,6),(6,7,5,4),(4,5,1,0),(1,5,7,2),(4,0,3,6))
colors = ((1,0,0),(0,1,0),(0,0,1),(0,1,0),(1,1,1),(0,1,1),(1,0,0),(0,1,0),(0,0,1),(1,0,0),(1,1,1),(0,1,1),)
def set_vertices(max_distance, min_distance = -20):
x_value_change = random.randrange(-10,10)
y_value_change = random.randrange(-10,10)
z_value_change = random.randrange(-1*max_distance,min_distance)
new_vertices = []
for vert in vertices:
new_vert = []
new_x = vert[0] + x_value_change
new_y = vert[1] + y_value_change
new_z = vert[2] + z_value_change
new_vert.append(new_x)
new_vert.append(new_y)
new_vert.append(new_z)
new_vertices.append(new_vert)
return new_vertices
def Cube(vertices):
glBegin(GL_QUADS)
for surface in surfaces:
x = 0
for vertex in surface:
x+=1
glColor3fv(colors[x])
glVertex3fv(vertices[vertex])
glEnd()
glBegin(GL_LINES)
for edge in edges:
for vertex in edge:
glVertex3fv(vertices[vertex])
glEnd()
def main():
pygame.init()
display = (800,600)
pygame.display.set_mode(display, DOUBLEBUF|OPENGL)
max_distance = 100
gluPerspective(45, (display[0]/display[1]), 0.1, max_distance)
glTranslatef(random.randrange(-5,5),random.randrange(-5,5), -40)
#object_passed = False
x_move = 0
y_move = 0
cube_dict = {}
for x in range(50):
cube_dict[x] =set_vertices(max_distance)
#glRotatef(25, 2, 1, 0)
x = glGetDoublev(GL_MODELVIEW_MATRIX)
camera_x = x[3][0]
camera_y = x[3][1]
camera_z = x[3][2]
button_down = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == pygame.MOUSEMOTION:
if button_down == True:
print(pygame.mouse.get_pressed())
glRotatef(event.rel[1], 1, 0, 0)
glRotatef(event.rel[0], 0, 1, 0)
for event in pygame.mouse.get_pressed():
# print(pygame.mouse.get_pressed())
if pygame.mouse.get_pressed()[0] == 1:
button_down = True
elif pygame.mouse.get_pressed()[0] == 0:
button_down = False
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
for each_cube in cube_dict:
Cube(cube_dict[each_cube])
pygame.display.flip()
pygame.time.wait(10)
main()
pygame.quit()
quit()
What you see as a 3D is actually a 2D game. After all, you are watching your screen, which (normally ;) ) is 2D. The virtual world (which is in 3D) is projected onto a plane, which is then shown on your screen. Our brains then convert that 2D image into a 3D one (like they do with the image of our eyes), making it look like it's 3D.
So it's relatively easy to make a 3D game: you just create a virtual world using a multidimensional matrix and then project it each loop on a 2D plane, which you display on your screen.
One tutorial that can put you on your way to 3D programs (using pygame) is this one .
3D rendering in Pygame without the help of other dependencies is hard to achieve and will not perform well. Pygame does not offer any functionality for drawing 3D shapes, meshes, or even perspective and lighting.
If you want to draw a 3D scene with Pygame, you need to compute the vertices using vector arithmetic and stitch the geometry together using polygons.
Example of then answer to Pygame rotating cubes around axis:
This approach won't give a satisfying performance and is only valuable for studying. 3D scenes are generated with the help of the GPU. A CPU-only approach does not achieve the required performance.
Nevertheless, nice results can be achieved with a 2.5-D approach. See the answer to How do I fix wall warping in my raycaster?:
import pygame
import math
pygame.init()
tile_size, map_size = 50, 8
board = [
'########',
'# # #',
'# # ##',
'# ## #',
'# #',
'### ###',
'# #',
'########']
def cast_rays(sx, sy, angle):
rx = math.cos(angle)
ry = math.sin(angle)
map_x = sx // tile_size
map_y = sy // tile_size
t_max_x = sx/tile_size - map_x
if rx > 0:
t_max_x = 1 - t_max_x
t_max_y = sy/tile_size - map_y
if ry > 0:
t_max_y = 1 - t_max_y
while True:
if ry == 0 or t_max_x < t_max_y * abs(rx / ry):
side = 'x'
map_x += 1 if rx > 0 else -1
t_max_x += 1
if map_x < 0 or map_x >= map_size:
break
else:
side = 'y'
map_y += 1 if ry > 0 else -1
t_max_y += 1
if map_x < 0 or map_y >= map_size:
break
if board[int(map_y)][int(map_x)] == "#":
break
if side == 'x':
x = (map_x + (1 if rx < 0 else 0)) * tile_size
y = player_y + (x - player_x) * ry / rx
direction = 'r' if x >= player_x else 'l'
else:
y = (map_y + (1 if ry < 0 else 0)) * tile_size
x = player_x + (y - player_y) * rx / ry
direction = 'd' if y >= player_y else 'u'
return (x, y), math.hypot(x - sx, y - sy), direction
def cast_fov(sx, sy, angle, fov, no_ofrays):
max_d = math.tan(math.radians(fov/2))
step = max_d * 2 / no_ofrays
rays = []
for i in range(no_ofrays):
d = -max_d + (i + 0.5) * step
ray_angle = math.atan2(d, 1)
pos, dist, direction = cast_rays(sx, sy, angle + ray_angle)
rays.append((pos, dist, dist * math.cos(ray_angle), direction))
return rays
area_width = tile_size * map_size
window = pygame.display.set_mode((area_width*2, area_width))
clock = pygame.time.Clock()
board_surf = pygame.Surface((area_width, area_width))
for row in range(8):
for col in range(8):
color = (192, 192, 192) if board[row][col] == '#' else (96, 96, 96)
pygame.draw.rect(board_surf, color, (col * tile_size, row * tile_size, tile_size - 2, tile_size - 2))
player_x, player_y = round(tile_size * 4.5) + 0.5, round(tile_size * 4.5) + 0.5
player_angle = 0
max_speed = 3
colors = {'r' : (196, 128, 64), 'l' : (128, 128, 64), 'd' : (128, 196, 64), 'u' : (64, 196, 64)}
run = True
while run:
clock.tick(30)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
keys = pygame.key.get_pressed()
hit_pos_front, dist_front, side_front = cast_rays(player_x, player_y, player_angle)
hit_pos_back, dist_back, side_back = cast_rays(player_x, player_y, player_angle + math.pi)
player_angle += (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * 0.1
speed = ((0 if dist_front <= max_speed else keys[pygame.K_UP]) - (0 if dist_back <= max_speed else keys[pygame.K_DOWN])) * max_speed
player_x += math.cos(player_angle) * speed
player_y += math.sin(player_angle) * speed
rays = cast_fov(player_x, player_y, player_angle, 60, 40)
window.blit(board_surf, (0, 0))
for ray in rays:
pygame.draw.line(window, (0, 255, 0), (player_x, player_y), ray[0])
pygame.draw.line(window, (255, 0, 0), (player_x, player_y), hit_pos_front)
pygame.draw.circle(window, (255, 0, 0), (player_x, player_y), 8)
pygame.draw.rect(window, (128, 128, 255), (400, 0, 400, 200))
pygame.draw.rect(window, (128, 128, 128), (400, 200, 400, 200))
for i, ray in enumerate(rays):
height = round(10000 / ray[2])
width = area_width // len(rays)
color = pygame.Color((0, 0, 0)).lerp(colors[ray[3]], min(height/256, 1))
rect = pygame.Rect(area_width + i*width, area_width//2-height//2, width, height)
pygame.draw.rect(window, color, rect)
pygame.display.flip()
pygame.quit()
exit()
Also see PyGameExamplesAndAnswers - Raycasting
I am aware that you asked "... but I was wondering if its possible to do very basic 3D graphics without any other dependencies.". Anyway, I will give you some additional options with other dependencies.
One way to make 3D scenes more powerful in Python is to use an OpenGL based library like pyglet or ModernGL.
However, you can use a Pygame window to create an OpenGL Context. You need to set the pygame.OPENGL flag when creating the display Surface (see pygame.display.set_mode):
window = pg.display.set_mode((width, height), pygame.OPENGL | pygame.DOUBLEBUF)
Modern OpenGL PyGame/PyOpenGL example:
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GL.shaders import *
import ctypes
import glm
glsl_vert = """
#version 330 core
layout (location = 0) in vec3 a_pos;
layout (location = 1) in vec4 a_col;
out vec4 v_color;
uniform mat4 u_proj;
uniform mat4 u_view;
uniform mat4 u_model;
void main()
{
v_color = a_col;
gl_Position = u_proj * u_view * u_model * vec4(a_pos.xyz, 1.0);
}
"""
glsl_frag = """
#version 330 core
out vec4 frag_color;
in vec4 v_color;
void main()
{
frag_color = v_color;
}
"""
class Cube:
def __init__(self):
v = [(-1,-1,-1), ( 1,-1,-1), ( 1, 1,-1), (-1, 1,-1), (-1,-1, 1), ( 1,-1, 1), ( 1, 1, 1), (-1, 1, 1)]
edges = [(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
surfaces = [(0,1,2,3), (5,4,7,6), (4,0,3,7),(1,5,6,2), (4,5,1,0), (3,2,6,7)]
colors = [(1,0,0), (0,1,0), (0,0,1), (1,1,0), (1,0,1), (1,0.5,0)]
line_color = [0, 0, 0]
edge_attributes = []
for e in edges:
edge_attributes += v[e[0]]
edge_attributes += line_color
edge_attributes += v[e[1]]
edge_attributes += line_color
face_attributes = []
for i, quad in enumerate(surfaces):
for iv in quad:
face_attributes += v[iv]
face_attributes += colors[i]
self.edge_vbo = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.edge_vbo)
glBufferData(GL_ARRAY_BUFFER, (GLfloat * len(edge_attributes))(*edge_attributes), GL_STATIC_DRAW)
self.edge_vao = glGenVertexArrays(1)
glBindVertexArray(self.edge_vao)
glVertexAttribPointer(0, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(3*ctypes.sizeof(GLfloat)))
glEnableVertexAttribArray(1)
self.face_vbos = glGenBuffers(1)
glBindBuffer(GL_ARRAY_BUFFER, self.face_vbos)
glBufferData(GL_ARRAY_BUFFER, (GLfloat * len(face_attributes))(*face_attributes), GL_STATIC_DRAW)
self.face_vao = glGenVertexArrays(1)
glBindVertexArray(self.face_vao)
glVertexAttribPointer(0, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, False, 6*ctypes.sizeof(GLfloat), ctypes.c_void_p(3*ctypes.sizeof(GLfloat)))
glEnableVertexAttribArray(1)
def draw(self):
glEnable(GL_DEPTH_TEST)
glLineWidth(5)
glBindVertexArray(self.edge_vao)
glDrawArrays(GL_LINES, 0, 12*2)
glBindVertexArray(0)
glEnable(GL_POLYGON_OFFSET_FILL)
glPolygonOffset( 1.0, 1.0 )
glBindVertexArray(self.face_vao)
glDrawArrays(GL_QUADS, 0, 6*4)
glBindVertexArray(0)
glDisable(GL_POLYGON_OFFSET_FILL)
def set_projection(w, h):
return glm.perspective(glm.radians(45), w / h, 0.1, 50.0)
pygame.init()
window = pygame.display.set_mode((400, 300), pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
clock = pygame.time.Clock()
proj = set_projection(*window.get_size())
view = glm.lookAt(glm.vec3(0, 0, 5), glm.vec3(0, 0, 0), glm.vec3(0, 1, 0))
model = glm.mat4(1)
cube = Cube()
angle_x, angle_y = 0, 0
program = compileProgram(
compileShader(glsl_vert, GL_VERTEX_SHADER),
compileShader(glsl_frag, GL_FRAGMENT_SHADER))
attrib = { a : glGetAttribLocation(program, a) for a in ['a_pos', 'a_col'] }
print(attrib)
uniform = { u : glGetUniformLocation(program, u) for u in ['u_model', 'u_view', 'u_proj'] }
print(uniform)
glUseProgram(program)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
elif event.type == pygame.VIDEORESIZE:
glViewport(0, 0, event.w, event.h)
proj = set_projection(event.w, event.h)
model = glm.mat4(1)
model = glm.rotate(model, glm.radians(angle_y), glm.vec3(0, 1, 0))
model = glm.rotate(model, glm.radians(angle_x), glm.vec3(1, 0, 0))
glUniformMatrix4fv(uniform['u_proj'], 1, GL_FALSE, glm.value_ptr(proj))
glUniformMatrix4fv(uniform['u_view'], 1, GL_FALSE, glm.value_ptr(view))
glUniformMatrix4fv(uniform['u_model'], 1, GL_FALSE, glm.value_ptr(model))
angle_x += 1
angle_y += 0.4
glClearColor(0.5, 0.5, 0.5, 1)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
cube.draw()
pygame.display.flip()
pygame.quit()
exit()
Legacy OpenGL PyGame/PyOpenGL example:
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *
class Cube:
def __init__(self):
self.v = [(-1,-1,-1), ( 1,-1,-1), ( 1, 1,-1), (-1, 1,-1), (-1,-1, 1), ( 1,-1, 1), ( 1, 1, 1), (-1, 1, 1)]
self.edges = [(0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7)]
self.surfaces = [(0,1,2,3), (5,4,7,6), (4,0,3,7),(1,5,6,2), (4,5,1,0), (3,2,6,7)]
self.colors = [(1,0,0), (0,1,0), (0,0,1), (1,1,0), (1,0,1), (1,0.5,0)]
def draw(self):
glEnable(GL_DEPTH_TEST)
glLineWidth(5)
glColor3fv((0, 0, 0))
glBegin(GL_LINES)
for e in self.edges:
glVertex3fv(self.v[e[0]])
glVertex3fv(self.v[e[1]])
glEnd()
glEnable(GL_POLYGON_OFFSET_FILL)
glPolygonOffset( 1.0, 1.0 )
glBegin(GL_QUADS)
for i, quad in enumerate(self.surfaces):
glColor3fv(self.colors[i])
for iv in quad:
glVertex3fv(self.v[iv])
glEnd()
glDisable(GL_POLYGON_OFFSET_FILL)
def set_projection(w, h):
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(45, w / h, 0.1, 50.0)
glMatrixMode(GL_MODELVIEW)
def screenshot(display_surface, filename):
size = display_surface.get_size()
buffer = glReadPixels(0, 0, *size, GL_RGBA, GL_UNSIGNED_BYTE)
screen_surf = pygame.image.fromstring(buffer, size, "RGBA")
pygame.image.save(screen_surf, filename)
pygame.init()
window = pygame.display.set_mode((400, 300), pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
clock = pygame.time.Clock()
set_projection(*window.get_size())
cube = Cube()
angle_x, angle_y = 0, 0
run = True
while run:
clock.tick(60)
take_screenshot = False
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
elif event.type == pygame.VIDEORESIZE:
glViewport(0, 0, event.w, event.h)
set_projection(event.w, event.h)
elif event.type == pygame.KEYDOWN:
take_screenshot = True
glLoadIdentity()
glTranslatef(0, 0, -5)
glRotatef(angle_y, 0, 1, 0)
glRotatef(angle_x, 1, 0, 0)
angle_x += 1
angle_y += 0.4
glClearColor(0.5, 0.5, 0.5, 1)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
cube.draw()
if take_screenshot:
screenshot(window, "cube.png")
pygame.display.flip()
pygame.quit()
exit()
You can make like this :
def convert_2d(x, y, z, horizon):
d = 1 - (z/horizon)
return x*d, y*d
def draw_list_of_points(lst):
'''Assume that lst is a list of 3 dimentionnal points like [(0, 0, 0), (1, 6, 2)...
Let's take 200 for the horizon, it can give us a pretty clean 3D'''
for x, y, z in lst:
pygame.draw.circle(screen, color, convert_2d(x, y, z, 200), 1)
But it's not very fast. If you want fast try to implement in C++/SDL2 or C.
Pygame is not very good for 3d graphics.
It is easy to make 3D driver for PyGame. PyGame has some assets for 3D game development.
I am developing Py3D driver using PyGame now. When I finish, I'll show you link to download Py3D. I tried to make 3D game with PyGame, and I needed just small addon for PyGame. It is wrong you think you must use SDL, PyOpenGL, OpenGL, PyQt5, Tkinter. All of them are wrong for making 3D games. OpenGL and PyOpenGL or Panda3D are very hard to learn. All my games made on those drivers were awful. PyQt5 and Tkinter aren't drivers for making games, but they've got addons for it. Don't try to make any game on those drivers. All drivers where we need to use the math module are hard. You can easily make small addon for them, I think everybody can make driver for PyGame in 1-2 weeks.
If you want to stick with a python-esque language when making games, Godot is a good alternative with both 2D and 3D support, a large community, and lots of tutorials. Its custom scripting language(gdscript) has some minor differences, but overall its mostly the same. It also has support for c# and c++, and has much more features when it comes to game development.
Pygame is just a library for changing the color of pixels (and some other useful stuff for programming games). You can do this by blitting images to the screen or directly setting the colors of pixels.
Because of this, it is easy to write 2D games with pygame, as the above is all you really need. But a 3D game is just some 3D objects 'squashed' (rendered) into 2D so that it can be displayed on the screen. So, to make a 3D game using only pygame, you would have handle this rendering by yourself, including all the complex matrix maths necessary.
Not only would this run slowly because of the immense processing power involved in this, but it would require you to write a massive 3D rendering/rasterisation engine. And because of python being interpreted it would be even slower. The correct approach would be to have this process run on the GPU using (Py)opengl.
So, yes it is technically possible to do 3D using only pygame, but definitely not recommended. I would suggest you learn Panda3D or some similar 3D engine.
Simple:
Just draw a bunch of polygons like:
import pygame
screen = pygame.display.set_mode((100, 100))
While True:
screen.fill((0, 0, 0))
Pos = [(10, 10), (20, 10), (20, 20), (10, 20)]
# first side (the front) in red
pygame.draw.polygon(screen, (225, 0, 0), Pos)
# outline in white
pygame.draw.lines(screen, (225, 225, 225), Pos)
# Second side (the back) in blue
Pos2 = [(Pos[0[0]] + 2.5, Pos[0[1]] + 2.5), (Pos2[0[0]] + 5, Pos2[0[1]]), (Pos2[1[0]], Pos2[1[1]] + 5), (Pos2[0[0]], Pos2[0[1]] + 5)]
pygame.draw.polygon(screen, (0, 0, 225), Pos2)
pygame.draw.lines(screen, (225, 225, 225), Pos2)
# Third side (the left but just 2 lines(not really)) in green
Pos3 = [Pos[0], Pos2[0], Pos2[3], Pos[3]]
pygame.draw.polygon(screen, (0, 225, 0), Pos3)
pygame.draw.lines(screen, (225, 225, 225), Pos3)
# Fourth side (the right) in purple
Pos4 = [Pos[1], Pos2[1], Pos2[2], Pos[2]]
pygame.draw.polygon(screen, (225, 0, 225), Pos4)
pygame.draw.lines(screen, (225, 225, 225), Pos4)
pygame.display.flip()
& there is a simple cube & I will soon provide a link for the full code to be able to rotate the cube & resize it
This should give you an equivalent of what you would get by using OpenGL
This is what I have managed to do with just Pygame and Numpy without using OpenGL with some basic shading.
You can do 3D in PyGame but probably isn't the most efficient and fastest.
Related
I'm learning how to create a multiplayer game with pygame by recreating Among-us.
I'm currently trying to recreate the vision system.
To make sure that the vision is blocked by walls, i've seen that a simple method is to place points at each corner of walls and to only draw the inner part of the triangles made between those points and the player.
(I'm aware that i'll have to stop the purple lines when they hit a wall.)
But my issue now is how can i draw the background (its an image of the Among_us map) only on those triangles and shade the rest of the image ?
PS: If you know how to integrate the shape bewteen the triangle and the and of the vision circle(like on the right of the picture) it would help to.
At first i thought about using the area parameter of blit but it only work with rectangles.
Maybe i could draw each pixel one by one but i don't think that it's a great idea.
I hope there is a better way.
To make sure that the vision is blocked by walls, i've seen that a simple method is to place points at each corner of walls and to only draw the inner part of the triangles made between those points and the player.
That's the way to go. Maybe you'll find this tutorial helpful.
But my issue now is how can i draw the background ... only on those triangles and shade the rest of the image ?
Create a black surface, "cut out" the visible area (for example, drawing a polygon with a color key will do), and just draw it on top; like Woodford already said in a comment.
PS: If you know how to integrate the shape bewteen the triangle and the and of the vision circle(like on the right of the picture) it would help to.
Here you can use the same idea. Create a black surface, draw a transparent circle with center = postion of the player and radius = 'max visible distance', and slap it on top.
Here I quickly ported the tutorial I linked to python/pygame:
# A port of https://ncase.me/sight-and-light/ to pygame
import pygame, math
from itertools import cycle,islice
from collections import namedtuple
FPS = 60
SCREEN_SIZE = 800, 600
SCREEN_RECT = pygame.Rect(0, 0, *SCREEN_SIZE)
COLOR_KEY = (12, 23, 34)
Point = namedtuple("Point", "x y")
Line = namedtuple("Ray", "a b")
Intersection = namedtuple("Intersection", "x y param angle", defaults=[0])
class Obstacle(pygame.sprite.Sprite):
def __init__(self, points, *args):
super().__init__(*args)
self.image = pygame.Surface(SCREEN_RECT.size)
self.image.set_colorkey(COLOR_KEY)
self.image.fill(COLOR_KEY)
self.rect = self.image.get_rect()
self.points = points
pygame.draw.polygon(self.image, (30, 255, 30), self.points, 4)
def __iter__(self):
return iter(self.points)
class Actor(pygame.sprite.Sprite):
def __init__(self, *args):
super().__init__(*args)
self.image = pygame.Surface((20, 20))
self.image.fill((255, 30, 30))
self.rect = self.image.get_rect(center=SCREEN_RECT.center)
def get_intersection(ray, segment):
"""Find intersection of RAY & SEGMENT"""
# RAY in parametric: Point + Delta*T1
r_px = ray.a.x
r_py = ray.a.y
r_dx = ray.b.x-ray.a.x
r_dy = ray.b.y-ray.a.y
# SEGMENT in parametric: Point + Delta*T2
s_px = segment.a.x
s_py = segment.a.y
s_dx = segment.b.x-segment.a.x
s_dy = segment.b.y-segment.a.y
# Are they parallel? If so, no intersect
r_mag = math.sqrt(r_dx*r_dx+r_dy*r_dy);
s_mag = math.sqrt(s_dx*s_dx+s_dy*s_dy);
if r_dx/r_mag==s_dx/s_mag and r_dy/r_mag==s_dy/s_mag:
return
try:
# SOLVE FOR T1 & T2
# r_px+r_dx*T1 = s_px+s_dx*T2 && r_py+r_dy*T1 = s_py+s_dy*T2
# ==> T1 = (s_px+s_dx*T2-r_px)/r_dx = (s_py+s_dy*T2-r_py)/r_dy
# ==> s_px*r_dy + s_dx*T2*r_dy - r_px*r_dy = s_py*r_dx + s_dy*T2*r_dx - r_py*r_dx
# ==> T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx)
T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx)
T1 = (s_px+s_dx*T2-r_px)/r_dx
# Must be within parametic whatevers for RAY/SEGMENT
if T1<0: return
if T2<0 or T2>1: return
# Return the POINT OF INTERSECTION
return Intersection(r_px+r_dx*T1, r_py+r_dy*T1, T1)
except ZeroDivisionError:
pass
def get_all_angles_from_points(start, points):
angles = []
for p in points:
angle = math.atan2(p[1]-start[1], p[0]-start[0])
angles.append(angle-0.0001)
angles.append(angle)
angles.append(angle+0.0001)
return angles
def get_blocker_surface(intersects):
intersects = sorted(intersects, key=lambda x: x.angle)
as_points = list(map(lambda r: (r.x, r.y), intersects))
blocker_surface = pygame.Surface(SCREEN_SIZE)
blocker_surface.set_colorkey(COLOR_KEY)
pygame.draw.polygon(blocker_surface, COLOR_KEY, as_points)
return blocker_surface
def fov(screen, start, obstacles):
points = set(p for o in obstacles for p in o)
angles = get_all_angles_from_points(start, points)
intersects = []
for angle in angles:
dx = math.cos(angle)
dy = math.sin(angle)
ray = Line(Point(start.x, start.y), Point(start.x + dx, start.y + dy))
closest_intersection = None
for o in obstacles:
connected_points = zip(o.points, islice(cycle(o.points), 1, None))
for segment in connected_points:
intersection = get_intersection(ray, Line._make(segment))
if not intersection: continue
if not closest_intersection or intersection.param < closest_intersection.param:
closest_intersection = intersection
if closest_intersection:
intersects.append(closest_intersection._replace(angle=angle))
return intersects
def main():
pygame.init()
pygame.display.set_caption("FOV demo")
clock = pygame.time.Clock()
screen = pygame.display.set_mode(SCREEN_SIZE)
sprites = pygame.sprite.Group()
obstacle_sprites = pygame.sprite.Group()
font = pygame.font.SysFont("Consolas", 20)
tmprect = SCREEN_RECT.inflate(-2, -2)
data = [
[tmprect.topleft, tmprect.topright, tmprect.bottomright, tmprect.bottomleft],
[(229, 211), (336, 294), (271, 406), (126, 323)],
[(386, 170), (487, 167), (393, 74), (309, 186)],
[(631, 502), (576, 457), (690, 389), (764, 493)],
[(519, 308), (489, 378), (589, 381), (532, 292)],
[(349, 552), (347, 480), (441, 484), (475, 566)],
[(637, 93), (592, 228), (726, 278), (661, 153)],
[(186, 494), (88, 431), (29, 533), (174, 566)],
[(97, 138), (32, 242), (150, 178), (161, 74)]
]
obstacles = [Obstacle([Point._make(t) for t in o], sprites, obstacle_sprites) for o in data]
player = Actor(sprites)
def stay_on_mouse(this):
this.rect.center = pygame.mouse.get_pos()
player.update = lambda *args: stay_on_mouse(player, *args)
do_fov = True
do_debug = False
do_draw_obstacles_on_top = True
fov_alpha = 255
while True:
evs = pygame.event.get()
for e in evs:
if e.type == pygame.QUIT:
return
if e.type == pygame.KEYDOWN:
if e.key == pygame.K_d: do_debug = not do_debug
if e.key == pygame.K_o: do_draw_obstacles_on_top = not do_draw_obstacles_on_top
if e.key == pygame.K_SPACE: do_fov = not do_fov
if e.key == pygame.K_ESCAPE: return
sprites.update()
screen.fill((50, 50, 50))
# draw regular stuff
sprites.draw(screen)
# hide stuff due to FOV
intersects = fov(screen, Point._make(player.rect.center), obstacles)
blocker_surface = get_blocker_surface(intersects)
fov_alpha = min(fov_alpha+5, 255) if do_fov else max(fov_alpha-5, 0)
blocker_surface.set_alpha(fov_alpha)
screen.blit(blocker_surface, (0, 0))
if do_debug:
for i in intersects:
pygame.draw.line(screen, (100, 100, 100), (i.x, i.y), player.rect.center)
if do_draw_obstacles_on_top:
obstacle_sprites.draw(screen)
y = 24
for line in ["[SPACE] - Toggle FOV", "[D] - Draw debug lines", "[O] - Draw obstascles on top"]:
screen.blit(font.render(line, True, (0, 0, 0)), (22, y+2))
screen.blit(font.render(line, True, (255, 255, 255)), (20, y))
y+= 24
pygame.display.flip()
clock.tick(FPS)
main()
And it looks like this:
That should get you started.
I am trying to model physics in pymunk and display using pygame and it works well so far with circles and a simple line. The problem I have is that I am trying to model a rectangle.
In pymunk that is a ploygon and I can create it using:-
def create_rect(space, pos):
body = pymunk.Body(1,100,body_type= pymunk.Body.DYNAMIC)
body.position = pos
poly_dims = [(100,10),(200,10),(200,15),(100,15)]
shape = pymunk.Poly(body,poly_dims)
space.add(body,shape)
return shape
I can get the position of the body which starts off falling and print using pygame with:-
pos_x = int(board.body.position.x)
pos_y = int(board.body.position.y)
pygame.draw.circle (screen,(0,0,0),(pos_x,pos_y),10)
But I cannot seem to obtain the moving co-ordinates of the Polygon edges to then print using pygame.
v = board.get_vertices()
print(v)
pygame.draw.polygon(screen,(0,0,0),v,1)
Vertices
[Vec2d(100.0, 10.0), Vec2d(200.0, 10.0), Vec2d(200.0, 15.0), Vec2d(100.0, 15.0)]
Which looks good, however the co-ordinates never change.
Then I tried:-
pos = board.body.position
print('Position')
print(pos)
which outputs
Position
Vec2d(530.5760282186911, 545.8604346347887)
And the position does change, but you cannot print the polygon with a 2D vector, you need all the vertices to do it. And presumably when the polygon hits a surface it will rotate etc. I just want to model a rectangle and I am stuck on this one point !
If you need the vertices in world coordinates then you have to transform the vertices. Follow the example in the Pymunk documentation (see get_vertices()) and write a function that draws a polygon from the transformed vertices:
def drawShape(surf, color, shape):
pts = []
for v in shape.get_vertices():
x, y = v.rotated(shape.body.angle) + shape.body.position
pts.append((round(x), round(surf.get_width() - y)))
pygame.draw.polygon(surf, color, pts, True)
Minimal example
import pygame, math
import pymunk
pygame.init()
screen = pygame.display.set_mode((200, 200))
def create_rect(space, pos, angle):
body = pymunk.Body(1, 100, body_type= pymunk.Body.DYNAMIC)
body.position = pos
body.angle = angle
poly_dims = [(-50, 0), (50, 0), (50, 5), (-50, 5)]
shape = pymunk.Poly(body,poly_dims)
space.add(body,shape)
return shape
def drawShape(surf, color, shape):
pts = []
for v in shape.get_vertices():
x, y = v.rotated(shape.body.angle) + shape.body.position
pts.append((round(x), round(surf.get_width() - y)))
pygame.draw.polygon(surf, color, pts, True)
space = pymunk.Space()
space.gravity = (0, 0)
board = create_rect(space, (100, 100), math.pi / 4)
clock = pygame.time.Clock()
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
screen.fill((255, 255, 255))
drawShape(screen, (255, 0, 0), board)
pygame.display.flip()
pygame.quit()
Alternatively you can use the pymunk.pygame_util Module. Minimal example:
import pygame, math
import pymunk
import pymunk.pygame_util
pygame.init()
screen = pygame.display.set_mode((200, 200))
draw_options = pymunk.pygame_util.DrawOptions(screen)
def create_rect(space, pos, angle):
body = pymunk.Body(1, 100, body_type= pymunk.Body.DYNAMIC)
body.position = pos
body.angle = angle
poly_dims = [(-50, 0), (50, 0), (50, 5), (-50, 5)]
shape = pymunk.Poly(body,poly_dims)
space.add(body,shape)
return shape
space = pymunk.Space()
space.gravity = (0, 0)
board = create_rect(space, (100, 100), math.pi / 4)
clock = pygame.time.Clock()
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
screen.fill((255, 255, 255))
space.debug_draw(draw_options)
pygame.display.flip()
pygame.quit()
I try to implement beam collision detection with a predefined track mask in Pygame. My final goal is to give an AI car model vision to see a track it's riding on:
This is my current code where I fire beams to mask and try to find an overlap:
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
# compute beam final point
x_dest = 250 + 500 * math.cos(math.radians(angle))
y_dest = 250 + 500 * math.sin(math.radians(angle))
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
hit = mask.overlap(beam_mask, (pos[0] - 250, pos[1] - 250))
if hit is not None:
pg.draw.line(surface, BLUE, mouse_pos, hit)
pg.draw.circle(surface, GREEN, hit, 3)
surface = pg.display.set_mode((500, 500))
mask_surface = pg.image.load("../assets/mask.png")
mask = pg.mask.from_surface(mask_surface)
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 120, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Let's describe what happens in the code snippet. One by one, I draw beams to beam_surface, make masks from them, and find overlap with background mask defined by one rectangle and a circle (black color in gifs). If there is a "hit point" (overlap point between both masks), I draw it with a line connecting hit point and mouse position.
It works fine for angles <0,90>:
But it's not working for angles in range <90,360>:
Pygame's overlap() documentation tells this:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
This means that this approach will work only if the beam hits the mask approximately from the top left corner. Do you have any advice on how to make it work for all of the situations? Is this generally a good approach to solve this problem?
Your approach works fine, if the x and y component of the ray axis points in the positive direction, but it fails if it points in the negative direction. As you pointed out, that is caused by the way pygame.mask.Mask.overlap works:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
To make the algorithm work, you have to ensure that the rays point always in the positive direction. Hence if the ray points in the negative x direction, then flip the mask and the ray vertical and if the ray points in the negative y direction than flip the ray horizontal.
Use pygame.transform.flip() top create 4 masks. Not flipped, flipped horizontal, flipped vertical and flipped vertical and horizontal:
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
Determine if the direction of the ray:
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
Get the flipped mask dependent on the direction of the ray:
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
Compute the flipped target point:
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
Compute the flipped offset:
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
Get the nearest intersection point of the flipped ray and mask and unflip the intersection point:
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 500 - hit[0] if flip_x else hit[0]
hy = 500 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, mouse_pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
See the example: repl.it/#Rabbid76/PyGame-PyGame-SurfaceLineMaskIntersect-2
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
#pg.draw.circle(surface, (255, 255, 0), mouse_pos, 3)
surface = pg.display.set_mode((500, 500))
#mask_surface = pg.image.load("../assets/mask.png")
mask_surface = pg.Surface((500, 500), pg.SRCALPHA)
mask_surface.fill((255, 0, 0))
pg.draw.circle(mask_surface, (0, 0, 0, 0), (250, 250), 100)
pg.draw.rect(mask_surface, (0, 0, 0, 0), (170, 170, 160, 160))
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 359, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Not,the algorithm can be further improved. The ray is always drawn on the bottom right quadrant of the beam_surface. Hence the other 3 quadrants are no longer needed and the size of beam_surface can be reduced to 250x250. The start of the ray is at (0, 0) rather than (250, 250) and the computation of the offsets hast to be slightly adapted:
beam_surface = pg.Surface((250, 250), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 500 * abs(c)
y_dest = 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (0, 0), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 499-pos[0] if flip_x else pos[0]
offset_y = 499-pos[1] if flip_y else pos[1]
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
I am trying to piece example code for a PyGame PyOpenGL tutorial into example code for a PyQt5 QOpenGLWidget. The goal of this code is to set up a cube with one corner skewed upward in order to identify the angle of the camera. It works fine in PyGame, but there are several problems with the PyQt5 version:
First, the aspect ratio seems to be off.
Second, the window recalls paintGL every time I make it active again.
Third, most of the variables are not transferring the same in regards to glTranslatef and glRotatef.
The code I am using for PyGame:
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
verticies = (
(1, -1, -1),
(1, 1, -1),
(-1, 1, -1),
(-1, -1, -1),
(1, -1, 2),
(1, 1, 1),
(-1, -1, 1),
(-1, 1, 1)
)
edges = (
(0,1),
(0,3),
(0,4),
(2,1),
(2,3),
(2,7),
(6,3),
(6,4),
(6,7),
(5,1),
(5,4),
(5,7)
)
colors = (
(1,0,0),
(0,1,0),
(0,0,1),
(0,1,0),
(1,1,1),
(0,1,1),
(1,0,0),
(0,1,0),
(0,0,1),
(1,0,0),
(1,1,1),
(0,1,1),
)
surfaces = (
(0,1,2,3),
(3,2,7,6),
(6,7,5,4),
(4,5,1,0),
(1,5,7,2),
(4,0,3,6)
)
def Cube():
glBegin(GL_QUADS)
for surface in surfaces:
x = 0
for vertex in surface:
x+=1
glColor3fv(colors[x])
glVertex3fv(verticies[vertex])
glEnd()
glBegin(GL_LINES)
for edge in edges:
for vertex in edge:
glVertex3fv(verticies[vertex])
glEnd()
def main():
pygame.init()
display = (800,600)
pygame.display.set_mode(display, DOUBLEBUF|OPENGL)
gluPerspective(45, (display[0]/display[1]), 0.1, 50.0)
glTranslatef(0,0, -10) #these two lines set the camera facing at the cube from the position 0, -10, 0.
glRotatef(-90, 2, 0, 0)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
glTranslatef(-0.5,0,0)
if event.key == pygame.K_RIGHT:
glTranslatef(0.5,0,0)
if event.key == pygame.K_UP:
glTranslatef(0,1,0)
if event.key == pygame.K_DOWN:
glTranslatef(0,-1,0)
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 4:
glTranslatef(0,0,1.0)
if event.button == 5:
glTranslatef(0,0,-1.0)
#glRotatef(1, 3, 1, 1) #rotation code that was commented out.
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
Cube()
pygame.display.flip()
pygame.time.wait(10)
main()
The result:
The PyQt5 code:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.uic import *
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *
class mainWindow(QMainWindow): #Main class.
verticies = (
(1, -1, -1),
(1, 1, -1),
(-1, 1, -1),
(-1, -1, -1),
(1, -1, 2),
(1, 1, 1),
(-1, -1, 1),
(-1, 1, 1)
)
edges = (
(0,1),
(0,3),
(0,4),
(2,1),
(2,3),
(2,7),
(6,3),
(6,4),
(6,7),
(5,1),
(5,4),
(5,7)
)
colors = (
(1,0,0),
(0,1,0),
(0,0,1),
(0,1,0),
(1,1,1),
(0,1,1),
(1,0,0),
(0,1,0),
(0,0,1),
(1,0,0),
(1,1,1),
(0,1,1),
)
surfaces = (
(0,1,2,3),
(3,2,7,6),
(6,7,5,4),
(4,5,1,0),
(1,5,7,2),
(4,0,3,6)
)
def __init__(self):
super(mainWindow, self).__init__()
self.width = 700 #Variables used for the setting of the size of everything
self.height = 600
self.setGeometry(0, 0, self.width, self.height) #Set the window size
def setupUI(self):
self.openGLWidget = QOpenGLWidget(self) #Create the GLWidget
self.openGLWidget.setGeometry(0, 0, self.width, self.height) #Size it the same as the window.
self.openGLWidget.initializeGL()
self.openGLWidget.resizeGL(self.width, self.height) #Resize GL's knowledge of the window to match the physical size?
self.openGLWidget.paintGL = self.paintGL #override the default function with my own?
def paintGL(self):
gluPerspective(45, self.width / self.height, 0.1, 50.0) #set perspective?
glTranslatef(0, 0, -2) #I used -10 instead of -2 in the PyGame version.
glRotatef(-90, 1, 0, 0) #I used 2 instead of 1 in the PyGame version.
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) #Straight from the PyGame version, with 'self' inserted occasionally
glBegin(GL_QUADS) #tell GL to draw surfaces
for surface in self.surfaces:
x = 0
for vertex in surface:
x+=1
glColor3fv(self.colors[x])
glVertex3fv(self.verticies[vertex])
glEnd() #tell GL to stop drawing surfaces
glBegin(GL_LINES) #tell GL to draw lines
for edge in self.edges:
for vertex in edge:
glVertex3fv(self.verticies[vertex])
glEnd() #tell GL to stop drawing lines.
app = QApplication([])
window = mainWindow()
window.setupUI()
window.show()
sys.exit(app.exec_())
The result:
When I switch to another window, then switch back to the Qt window, the scene updates and paintGL is called again. Also, the cube appears to be squashed and the camera acts differently. What can I do to fix these?
Python 3.8
Windows 10
The OpenGL matrix operations (like gluPerspective, glTranslate, glRotate, ...) do not just set a matrix. The operations define a new matrix and multiply the current matrix by the new matrix. The causes that the matrix continuously and progressively changes, every time when paintGL is called.
The issue can be solved with ease, by loading the identity matrix by glLoadIdentity at the begin of paintGL::
class mainWindow(QMainWindow):
# [...]
def paintGL(self):
glLoadIdentity()
gluPerspective(45, self.width / self.height, 0.1, 50.0) #set perspective?
glTranslatef(0, 0, -10) #I used -10 instead of -2 in the PyGame version.
glRotatef(-90, 1, 0, 0) #I used 2 instead of 1 in the PyGame version.
But Legacy OpenGL provides different matrices (see glMatrixMode).
It is recommend to pout the projection matrix to the current GL_PROJECTION matrix and the model view matrix to the current GL_MODELVIEW matrix.
Update the viewport rectangle (glViewport) and the projection matrix in the resize event callback (resizeGL). Set the model view matrix in paintGL:
class mainWindow(QMainWindow):
# [...]
def setupUI(self):
# [...]
self.openGLWidget.paintGL = self.paintGL
self.openGLWidget.resizeGL = self.resizeGL
def resizeGL(self, width, height):
self.width, self.height = width, height
# update viewport
glViewport(0, 0, self.width, self.height)
# set projection matrix
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
gluPerspective(45, self.width / self.height, 0.1, 50.0) #set perspective?
def paintGL(self):
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glTranslatef(0, 0, -10) #I used -10 instead of -2 in the PyGame version.
glRotatef(-90, 1, 0, 0) #I used 2 instead of 1 in the PyGame version.
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT) #Straight from the PyGame version, with 'self' inserted occasionally
glBegin(GL_QUADS) #tell GL to draw surfaces
for surface in self.surfaces:
x = 0
for vertex in surface:
x+=1
glColor3fv(self.colors[x])
glVertex3fv(self.verticies[vertex])
glEnd() #tell GL to stop drawing surfaces
glBegin(GL_LINES) #tell GL to draw lines
for edge in self.edges:
for vertex in edge:
glVertex3fv(self.verticies[vertex])
glEnd() #tell GL to stop drawing lines.
I am creating a randomly generated map in with PyGame. However, I've run into an issue, where, if the user scrolls away from the top-left corner of the map and changes the PyGame surface that's displayed, an issue happens.
The problem is, PyGame still starts them on the upper-left of the surface, and will then allow them to scroll off the edges of the surface because the list that keeps track of that, camera_pos, now has incorrect values.
All of the surfaces are the same dimensions and I want to make it so the user is in the same position when they change the displayed surface. However, I'm not sure how to set the position of the user's view when pygame switches surfaces.
How can I switch the position of the user's view back to what it used to be when the surface is switched?
I have made a MCV Example below I hope will help. Instead of displaying maps, it just draws a border around a solid color. I apologize for how long it is. I'm not sure how to make it much shorter.
In this example, scrolling is done with the arrow keys. You can press r, g, or b on the keyboard to display the different colored surfaces.
import pygame
import numpy as np
import sys
def scroll_y(display_surface, offset):
"""
Handles vertical scrolling.
:param display_surface: A pyGame surface object.
:param offset: The speed of the scroll
"""
width, height = display_surface.get_size()
map_copy = display_surface.copy()
display_surface.blit(map_copy, (0, offset))
# handle scrolling down
if offset < 0:
display_surface.blit(map_copy,
(0, height + offset),
(0, 0, width, -offset))
# handle scrolling up
else:
display_surface.blit(map_copy,
(0, 0),
(0, height - offset, width, offset))
def scroll_x(display_surface, offset):
"""
Handles horizontal scrolling.
:param display_surface: A pyGame surface object.
:param offset: The speed of the scroll
"""
width, height = display_surface.get_size()
map_copy = display_surface.copy()
display_surface.blit(map_copy, (offset, 0))
# handle scrolling right
if offset < 0:
display_surface.blit(map_copy,
(width + offset, 0),
(0, 0, -offset, height))
# handle scrolling left
else:
display_surface.blit(map_copy,
(0, 0),
(width - offset, 0, offset, height))
def main():
"""
This function displays the three surfaces.
Press r to show the red surface (which is displayed by default).
Press g to show the green surface.
Press b to show the blue surface.
"""
pygame.init()
window = pygame.display.set_mode((1600, 900))
red_surface = pygame.Surface([3200, 1800]).convert(window)
green_surface = pygame.Surface([3200, 1800]).convert(window)
blue_surface = pygame.Surface([3200, 1800]).convert(window)
red_surface.fill((255, 145, 145))
green_surface.fill((145, 255, 145))
blue_surface.fill((145, 145, 255))
# draw thick black lines on surface borders
pygame.draw.rect(red_surface, (0, 0, 0), (0, 0, 3200, 1800), 40)
pygame.draw.rect(green_surface, (0, 0, 0), (0, 0, 3200, 1800), 40)
pygame.draw.rect(blue_surface, (0, 0, 0), (0, 0, 3200, 1800), 40)
display_surface = red_surface.copy()
camera_pos = np.array([0, 0])
while True: # <-- the pyGame loop
event = pygame.event.poll()
pressed = pygame.key.get_pressed()
# handle closing the window
if event.type == pygame.QUIT:
break
window.blit(display_surface, (0, 0))
# handle switching display modes
if pressed[pygame.K_g]:
display_surface = green_surface
elif pressed[pygame.K_b]:
display_surface = blue_surface
elif pressed[pygame.K_r]:
display_surface = red_surface
# handle scrolling, make sure you can't scroll past the borders
if pressed[pygame.K_UP] and camera_pos[1] > 0:
scroll_y(display_surface, 5)
camera_pos[1] -= 5
elif pressed[pygame.K_DOWN] and camera_pos[1] < (1800 / 2):
scroll_y(display_surface, -5)
camera_pos[1] += 5
elif pressed[pygame.K_LEFT] and camera_pos[0] > 0:
scroll_x(display_surface, 5)
camera_pos[0] -= 5
elif pressed[pygame.K_RIGHT] and camera_pos[0] < (3200 / 2):
scroll_x(display_surface, -5)
camera_pos[0] += 5
# updates what the window displays
pygame.display.update()
pygame.quit()
sys.exit(0)
if __name__ == "__main__":
# runs the pyGame loop
main()
Here's what I think is a fairly elegant solution that doesn't require the two scrolling functions, scroll_x() and scroll_y() you have. Because it was so fast not using them, the main loop was detecting the same scrolling key as being pressed multiple times — necessitating the addition of a pygame.time.Clock to slow the frame-rate down to something reasonable.
Instead of scrolling the display surfaces themselves via those scrolling functions, as your code was doing, this version just updates the current "camera" position, then blits the corresponding region of the current display_surface to the window whenever it's modified. The camera's position is constrained by making sure its x and y components stay within some boundary limit constants — MINX,MINY and MAXX,MAXY — which get computed based the values of some other previously defined constants.
The use of symbolic constants rather than hardcoding literal values multiple places in the code is considered a very good programming practice because it makes changing them easier, since doing so only requires a source code change to be done one place.
import pygame
import sys
def main():
"""
This function displays the three surfaces.
Press r to show the red surface (which is displayed by default).
Press g to show the green surface.
Press b to show the blue surface.
"""
FPS = 60 # Frames per second
SURF_WIDTH, SURF_HEIGHT = 3200, 1800
WIN_WIDTH, WIN_HEIGHT = 1600, 900
DX, DY = 5, 5 # Scroll amounts.
MINX, MAXX = DX, SURF_WIDTH - WIN_WIDTH + DX - 1
MINY, MAXY = DY, SURF_HEIGHT - WIN_HEIGHT + DY - 1
pygame.init()
pygame.font.init()
fonts = pygame.font.get_fonts()
clock = pygame.time.Clock()
window = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
red_surface = pygame.Surface([SURF_WIDTH, SURF_HEIGHT]).convert(window)
green_surface = pygame.Surface([SURF_WIDTH, SURF_HEIGHT]).convert(window)
blue_surface = pygame.Surface([SURF_WIDTH, SURF_HEIGHT]).convert(window)
red_surface.fill((255, 145, 145))
green_surface.fill((145, 255, 145))
blue_surface.fill((145, 145, 255))
# Draw thick black lines on surface borders
pygame.draw.rect(red_surface, (0, 0, 0), (0, 0, SURF_WIDTH, SURF_HEIGHT), 40)
pygame.draw.rect(green_surface, (0, 0, 0), (0, 0, SURF_WIDTH, SURF_HEIGHT), 40)
pygame.draw.rect(blue_surface, (0, 0, 0), (0, 0, SURF_WIDTH, SURF_HEIGHT), 40)
# Draw label on each of the surfaces for testing. (ADDED)
font = pygame.font.SysFont(None, 35)
rtext = font.render('red surface', True, (255, 0, 0))
textpos = rtext.get_rect(centerx=300, centery=200) # Reused.
red_surface.blit(rtext, textpos)
rtext = font.render('green surface', True, (0, 192, 0))
green_surface.blit(rtext, textpos)
rtext = font.render('blue surface', True, (0, 0, 255))
blue_surface.blit(rtext, textpos)
display_surface = red_surface
camera_pos = pygame.math.Vector2(0, 0)
update_surface = True
while True: # Game loop
if update_surface:
window.blit(display_surface, (0, 0), (camera_pos[0], camera_pos[1],
WIN_WIDTH, WIN_HEIGHT))
update_surface = False
event = pygame.event.poll()
pressed = pygame.key.get_pressed()
# Close window?
if event.type == pygame.QUIT or pressed[pygame.K_ESCAPE]:
break
# Switch display surface?
if pressed[pygame.K_g]:
display_surface = green_surface
update_surface = True
elif pressed[pygame.K_b]:
display_surface = blue_surface
update_surface = True
elif pressed[pygame.K_r]:
display_surface = red_surface
update_surface = True
# Constrain scrolling to within borders
if pressed[pygame.K_LEFT] and camera_pos[0] >= MINX:
camera_pos[0] -= DX
update_surface = True
elif pressed[pygame.K_RIGHT] and camera_pos[0] <= MAXX:
camera_pos[0] += DX
update_surface = True
elif pressed[pygame.K_UP] and camera_pos[1] >= MINY:
camera_pos[1] -= DY
update_surface = True
elif pressed[pygame.K_DOWN] and camera_pos[1] <= MAXY:
camera_pos[1] += DY
update_surface = True
# updates what the window displays
pygame.display.update()
clock.tick(FPS)
pygame.quit()
sys.exit(0)
if __name__ == "__main__":
main() # runs the pyGame loop