ソースコード
"""
空間遊泳
第1.0版 2025.4.2
第1.1版 2025.4.3 移動時加速の調整、ビットマップ縮小レベルの調整
MIT License
Copyright (c) 2025 Current Color Co. Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import pygame
import math
import random
import numpy
import datetime
import gc
# 画面設定
G_WIDTH = 800
G_HEIGHT = 600
pygame.display.set_caption("空間遊泳")
# 空間設定
S_WIDTH = 500
S_HEIGHT = 500
S_DEPTH = 1000
# 色の定義
C_BLACK = (0, 0, 0)
C_WHITE = (255, 255, 255)
C_YELLOW = (255, 255, 0)
C_GRAY = (128, 128, 128)
C_BLUE = (0, 255, 255)
C_RED = (255, 0, 0)
# オブジェクトの種類
OBJECT_TYPE_CIRCLE = 0
OBJECT_TYPE_TRIANGLE = 1
OBJECT_TYPE_LINE = 2
OBJECT_TYPE_RECTANGLE = 3
OBJECT_TYPE_TEXT = 4
OBJECT_TYPE_BITMAP = 5
OBJECT_TYPE_TETRAHEDRON = 6
OBJECT_TYPE_PILLAR = 7
# グローバル変数
######################################
# 3D から 2D への変換関数
def project_point(x1, y1, z1, xc, yc, zc, rot_UD, rot_RL, rot_Z):
# カメラ基準の座標系に変換
dx, dy, dz = x1 - xc, y1 - yc, z1 - zc
# 左右・上下・Z軸の回転
cos_r, sin_r = math.cos(math.radians(rot_RL)), math.sin(math.radians(rot_RL))
dx, dz = cos_r * dx - sin_r * dz, sin_r * dx + cos_r * dz
cos_u, sin_u = math.cos(math.radians(rot_UD)), math.sin(math.radians(rot_UD))
dy, dz = cos_u * dy - sin_u * dz, sin_u * dy + cos_u * dz
cos_z, sin_z = math.cos(math.radians(rot_Z)), math.sin(math.radians(rot_Z))
dx, dy = cos_z * dx - sin_z * dy, sin_z * dx + cos_z * dy
if dz <= 0: #カメラより後ろのオブジェクト
return None, None, None, None
# 透視投影
fov = 500 # 視野のスケール
x2 = int(G_WIDTH / 2 + (dx / dz) * fov)
y2 = int(G_HEIGHT / 2 - (dy / dz) * fov)
d = math.sqrt(dx**2 + dy**2 + dz**2)
r = fov / dz
return x2, y2, d, r
######################################
# 三角形の裏表判定
# 三角形の表側がカメラ から見えているか判定する。
# 三角形の頂点は表側から見た時に右回りに定義する。
def is_triangle_visible(points_3d, camera):
(x1, y1, z1) = points_3d[0]
(x2, y2, z2) = points_3d[1]
(x3, y3, z3) = points_3d[2]
xc = camera[0]
yc = camera[1]
zc = camera[2]
v1 = numpy.array([x2 - x1, y2 - y1, z2 - z1])
v2 = numpy.array([x3 - x1, y3 - y1, z3 - z1])
normal = numpy.cross(v1, v2) #三角形の法線ベクトル(外積)
xg = (x1 + x2 + x3) / 3
yg = (y1 + y2 + y3) / 3
zg = (z1 + z2 + z3) / 3
view_vector = numpy.array([xg - xc, yg - yc, zg - zc]) #視線ベクトル
dot_product = numpy.dot(normal, view_vector) #内積
return dot_product <= 0 # Trueなら表側
######################################
# 四角形の裏表判定
# 四角形の表側がカメラ から見えているか判定する。
# 四角形の頂点は同一平面内で表側から見た時に右回りに定義する。
def is_rectangle_visible(points_3d, camera):
(x1, y1, z1) = points_3d[0]
(x2, y2, z2) = points_3d[1]
(x3, y3, z3) = points_3d[2]
(x4, y4, z4) = points_3d[3]
xc = camera[0]
yc = camera[1]
zc = camera[2]
v1 = numpy.array([x2 - x1, y2 - y1, z2 - z1])
v2 = numpy.array([x3 - x1, y3 - y1, z3 - z1])
normal = numpy.cross(v1, v2) #三角形の法線ベクトル(外積)
xg = (x1 + x2 + x3 + x4) / 4
yg = (y1 + y2 + y3 + y4) / 4
zg = (z1 + z2 + z3 + z4) / 4
view_vector = numpy.array([xg - xc, yg - yc, zg - zc]) #視線ベクトル
dot_product = numpy.dot(normal, view_vector) #内積
return dot_product <= 0 # Trueなら表側
######################################
# 多角形の描画(3D→2D)
def draw_polygon(screen, points_3d, camera, color, line=None):
points_2d = []
for (x, y, z) in points_3d:
x1, y1, d, r = project_point(x, y, z, *camera)
if x1 is None:
return
else:
points_2d.append((x1,y1))
if line is not None:
pygame.draw.polygon(screen, color, points_2d, line)
else:
pygame.draw.polygon(screen, color, points_2d)
######################################
# ビットマップ(Surface)のキャッシュ管理クラス
class ImageCache:
def __init__(self):
self.cache = {}
self.levels = numpy.linspace(0,1.1,25)**1.8
def get_transformed_image(self, img, type, scale, angle):
#回転角、拡大率の離散化
angle = round(angle/7.5)*7.5 %360
scale = min(self.levels, key=lambda x:abs(x - scale))
key = (type, scale, angle)
if key in self.cache:
return self.cache[key]
img1 = pygame.transform.scale(img,
(int(img.get_width() * scale), int(img.get_height() * scale)))
img2 = pygame.transform.rotate(img1, angle)
self.cache[key] = img2
del img1
return img2
def clear(self):
self.cache.clear()
gc.collect()
def clear_type(self, type):
keys_to_delete = [key for key in self.cache if key[0] == type]
for key in keys_to_delete:
del self.cache[key]
gc.collect()
######################################
# オブジェクトのクラス
class OBJECT:
# オブジェクトの生成
def __init__(self, type):
self.type = type
self.x = 0
self.y = 0
self.vx = 0
self.vy = 0
self.z = S_DEPTH
self.color = C_BLACK
self.image = pygame.image.load("image.png")
self.text = ""
if self.type == OBJECT_TYPE_CIRCLE:
self.x = random.randint(-500,500)
self.y = random.randint(200,500)
self.color = (128,196,255)
elif self.type == OBJECT_TYPE_TRIANGLE:
self.x = random.randint(-500,500)
self.y = random.randint(100,200)
self.color = (255,64,64)
self.cycle = 0
self.x_center = self.x
elif self.type == OBJECT_TYPE_TETRAHEDRON:
self.x = random.randint(-250,250)
self.y = random.randint(0,100)
self.color = (128,255,128)
elif self.type == OBJECT_TYPE_TEXT:
self.x = 0
self.y = 200
self.color = (255,255,128)
self.font = pygame.font.SysFont("Courier", 48)
elif self.type == OBJECT_TYPE_BITMAP:
self.x = random.randint(-300,300)
self.y = random.randint(-100,300)
elif self.type == OBJECT_TYPE_LINE:
self.color = (255,255,255)
self.x = 0
self.y = -S_HEIGHT
elif self.type == OBJECT_TYPE_PILLAR:
self.color = (255,255,255)
self.x = random.randint(-300,300)
self.y = 0
self.height = random.randint(-100,-30)
# オブジェクトの移動
def move(self, dz, tx, ty):
#オフセット
self.z += dz
if self.type == OBJECT_TYPE_CIRCLE:
pass
elif self.type == OBJECT_TYPE_TRIANGLE:
self.cycle += 10
if self.cycle > 360:
self.cycle -= 360
self.x = self.x_center + 30*math.cos(math.radians(self.cycle))
elif self.type == OBJECT_TYPE_TETRAHEDRON:
self.z -= 15
elif self.type == OBJECT_TYPE_TEXT:
pass
elif self.type == OBJECT_TYPE_BITMAP:
a = 0.3 #加速度
d = 50 #非加速範囲
self.vx += a if self.x < tx - d else (-a if self.x > tx + d else 0)
self.vy += a if self.y < ty - d else (-a if self.y > ty + d else 0)
self.vx, self.vy = self.vx * 0.95, self.vy *0.95
self.x += self.vx
self.y += self.vy
self.z -= 8
elif self.type == OBJECT_TYPE_LINE:
pass
elif self.type == OBJECT_TYPE_PILLAR:
pass
# オブジェクトの描画
def draw(self, screen, x2, y2, d, r, camera, imgc):
if self.type == OBJECT_TYPE_CIRCLE:
cr = (S_DEPTH - self.z)/S_DEPTH
c = tuple(element * cr for element in self.color)
pygame.draw.circle(screen, c, (x2, y2), max(2, int(30 * r)))
elif self.type == OBJECT_TYPE_TRIANGLE:
cr = (S_DEPTH - self.z)/S_DEPTH
c = tuple(element * cr for element in self.color)
points_3d = [(self.x - 30, self.y, self.z),
(self.x + 30, self.y, self.z),
(self.x, self.y -60, self.z)]
draw_polygon(screen, points_3d, camera, c)
elif self.type == OBJECT_TYPE_TETRAHEDRON:
s = 100
h = (math.sqrt(6) / 3) * s
z_min = self.z - (3/4)*h
z_max = self.z + (1/4)*h
v1 = (self.x, self.y, z_min)
v2 = (self.x-s/2, self.y - s/(2*math.sqrt(3)), z_max)
v3 = (self.x+s/2, self.y - s/(2*math.sqrt(3)), z_max)
v4 = (self.x, self.y + s / math.sqrt(3), z_max)
"""
points_3d = [v4, v2, v3]
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.5
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
"""
points_3d = [v4, v1, v2]
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
points_3d = [v1, v4, v3]
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.8
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
points_3d = [v1, v3, v2]
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.7
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
elif self.type == OBJECT_TYPE_TEXT:
text = str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
if self.text != text:
self.text = text
self.image = self.font.render(text, True, C_YELLOW)
imgc.clear_type(OBJECT_TYPE_TEXT)
img = imgc.get_transformed_image(self.image,
OBJECT_TYPE_TEXT, r * 0.2, 0)
if self.z > S_DEPTH /2:
cr = (S_DEPTH - self.z)/(S_DEPTH / 2)
img.set_alpha(cr *255)
rect = img.get_rect()
rect.center = (x2, y2)
screen.blit(img, rect)
elif self.type == OBJECT_TYPE_BITMAP:
r1 = r * 0.2
img = imgc.get_transformed_image(self.image,
OBJECT_TYPE_BITMAP, r1, camera[5])
if self.z > S_DEPTH /2:
cr = (S_DEPTH - self.z)/(S_DEPTH / 2)
img.set_alpha(cr *255)
rect = img.get_rect()
rect.center = (x2, y2)
screen.blit(img, rect)
elif self.type == OBJECT_TYPE_LINE:
cr = (S_DEPTH - self.z)/S_DEPTH
c = tuple(element * cr for element in self.color)
points_3d = [(self.x - S_WIDTH, self.y, self.z),
(self.x + S_WIDTH, self.y, self.z)]
draw_polygon(screen, points_3d, camera, c, 5)
elif self.type == OBJECT_TYPE_PILLAR:
cr = (S_DEPTH - self.z)/S_DEPTH
c = tuple(element * cr for element in self.color)
size = 20
x_min = self.x - size
x_max = self.x + size
z_min = self.z - size
z_max = self.z + size
y_min = -S_HEIGHT * 0.7
y_max = self.height
v1 = (x_min, y_max, z_max)
v2 = (x_max, y_max, z_max)
v3 = (x_max, y_max, z_min)
v4 = (x_min, y_max, z_min)
v5 = (x_min, y_min, z_max)
v6 = (x_max, y_min, z_max)
v7 = (x_max, y_min, z_min)
v8 = (x_min, y_min, z_min)
"""
points_3d = [v2, v1, v5, v6] #背面
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.8
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
"""
points_3d = [v3, v2, v6, v7] #右面
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.8
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
points_3d = [v4, v3, v7, v8] #前面
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.9
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
points_3d = [v1, v4, v8, v5] #左面
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
points_3d = [v1, v2, v3, v4] #上面
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.7
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
points_3d = [v6, v5, v8, v7] #下面
if is_triangle_visible(points_3d, camera):
cr = (S_DEPTH - self.z)/S_DEPTH * 0.6
c = tuple(element * cr for element in self.color)
draw_polygon(screen, points_3d, camera, c)
######################################
# 背景のクラス
class BACKGROUND:
# 背景の描画
def draw(self, screen, camera):
points_3d = [(-S_WIDTH, -S_HEIGHT, S_DEPTH * 2),
( S_WIDTH, -S_HEIGHT, S_DEPTH *2),
( S_WIDTH, S_HEIGHT, S_DEPTH *2),
(-S_WIDTH, S_HEIGHT, S_DEPTH *2)]
draw_polygon(screen, points_3d, camera, (128,128,128), 1)
######################################
# 自己視点のクラス
class PLAYER:
def __init__(self):
self.vx = 0
self.vy = 0
self.v_max = 10
self.v_attenu = 0.9
self.rz_max = 45
self.rx_mx = 30
self.ry_max = 30
def move(self, ax, ay, camera):
x,y = camera[0],camera[1]
rx,ry,rz = camera[4],camera[3],camera[5]
self.vx += ax
self.vx *= self.v_attenu
self.vx = min(self.v_max, self.vx)
self.vx = max(-self.v_max, self.vx)
self.vy += ay
self.vy *= self.v_attenu
self.vy = min(self.v_max, self.vy)
self.vy = max(-self.v_max, self.vy)
rx = self.vx * 5
rz = self.vx * 3
ry = self.vy * 5
x += self.vx
y += self.vy
camera[0],camera[1] = x,y
camera[4],camera[3],camera[5] = rx, ry, rz
######################################
# メイン関数
def main():
# 初期化
pygame.init()
screen = pygame.display.set_mode((G_WIDTH, G_HEIGHT))
clock = pygame.time.Clock()
camera = [0,0,0,0,0,0] #[x,y,z, up_down, right_left, z]
objects = []
bg = BACKGROUND()
player = PLAYER()
imgc = ImageCache()
gen_types = [OBJECT_TYPE_CIRCLE,
OBJECT_TYPE_TRIANGLE,
OBJECT_TYPE_TETRAHEDRON,
OBJECT_TYPE_LINE,
OBJECT_TYPE_TEXT,
OBJECT_TYPE_PILLAR,
OBJECT_TYPE_BITMAP]
gen_counts = [0,0,0,0,0,0,0]
gen_periods = [3,10,15,30,30,10,20]
running = True
while running:
screen.fill(C_BLACK)
# 入力処理
keys = pygame.key.get_pressed()
if keys[pygame.K_g]: camera[0] -= 10
if keys[pygame.K_j]: camera[0] += 10
if keys[pygame.K_y]: camera[1] += 10
if keys[pygame.K_h]: camera[1] -= 10
if keys[pygame.K_a]: camera[4] = max(-45, camera[4] - 1)
if keys[pygame.K_d]: camera[4] = min(45, camera[4] + 1)
if keys[pygame.K_w]: camera[3] = min(45, camera[3] + 1)
if keys[pygame.K_s]: camera[3] = max(-45, camera[3] - 1)
if keys[pygame.K_x]: camera[5] -= 1
if keys[pygame.K_c]: camera[5] += 1
if keys[pygame.K_r]: camera[2] += 23
if keys[pygame.K_f]: camera[2] -= 23
if keys[pygame.K_q]: camera = [0, 0, 0, 0, 0, 0]
ax, ay = 0,0
accel = 0.3
if keys[pygame.K_LEFT]: ax = -accel
if keys[pygame.K_RIGHT]: ax = accel
if keys[pygame.K_UP]: ay = accel
if keys[pygame.K_DOWN]: ay = -accel
# 背景の描画
bg.draw(screen, camera)
# オブジェクトの生成
for i, type in enumerate(gen_types):
gen_counts[i] -= 1
if gen_counts[i] <= 0:
gen_counts[i] = gen_periods[i]
objects.append(OBJECT(type))
# オブジェクトの移動と削除
speed = -5
for object in reversed(objects):
object.move(speed, camera[0], camera[1])
if object.z < 100:
objects.remove(object)
# カメラ位置の移動制御
player.move(ax,ay,camera)
# オブジェクトの描画 (遠い順にソート)
projected_objects = []
for object in objects:
x2, y2, d, r = project_point(object.x, object.y, object.z, *camera)
if x2 is not None:
projected_objects.append([object, x2, y2, d, r])
projected_objects.sort(reverse=True, key=lambda x: x[3])
for object, x2, y2, d, r in projected_objects:
object.draw(screen, x2, y2, d, r, camera, imgc)
pygame.display.flip()
clock.tick(30)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
if __name__ == "__main__":
main()