Make basic 3D "game" with two shapes
dkl9

dkl9 commited on 2025-170 10:42:16
Showing 5 changed files, with 271 additions and 0 deletions.

... ...
@@ -0,0 +1,81 @@
1
+from libs import *
2
+
3
+# https://iquilezles.org/articles/intersectors/
4
+def intersect_tri(ro, rd, tv):
5
+    e1 = tv[1] - tv[0]
6
+    e2 = tv[2] - tv[0]
7
+    to = ro - tv[0]
8
+    n = np.cross(e1, e2)
9
+    q = np.cross(to, rd)
10
+    d = 1.0 / np.dot(rd, n)
11
+    u = d * np.dot(-q, e2)
12
+    v = d * np.dot(q, e1)
13
+    t = d * np.dot(-n, to)
14
+    if u > 0.0 and v > 0.0 and u + v < 1.0 and t > 1e-3:
15
+        return t
16
+
17
+class Camera:
18
+    def __init__(self, **kwargs):
19
+        self.pos = vec([0, 0, 0])
20
+        self.theta = 0
21
+        self.phi = 0
22
+        self.fovy = kwargs.get("fovy", 90)
23
+        self.aspect = 1
24
+
25
+    def looking(self):
26
+        return vec([
27
+            np.cos(self.phi) * np.sin(self.theta),
28
+            np.sin(self.phi),
29
+            np.cos(self.phi) * np.cos(self.theta)
30
+        ])
31
+
32
+    def up(self):
33
+        return vec([0, 1, 0])
34
+
35
+    def view_mat(self):
36
+        f = normalize(self.looking())
37
+        s = normalize(np.cross(f, self.up()))
38
+        u = np.cross(s, f)
39
+        m = np.identity(4, dtype=FLOAT)
40
+        m[:3, :3] = [s, u, -f]
41
+        m[:3, 3] = [-np.dot(s, self.pos), -np.dot(u, self.pos), np.dot(f, self.pos)]
42
+        return m
43
+
44
+    def persp_mat(self):
45
+        near = 0.1
46
+        far = 100
47
+        f = 1.0 / np.tan(np.radians(self.fovy) / 2)
48
+        depth = near - far
49
+        return vec([
50
+            [f / self.aspect, 0, 0, 0],
51
+            [0, f, 0, 0],
52
+            [0, 0, (near + far) / depth, (2 * near * far) / depth],
53
+            [0, 0, -1, 0]
54
+        ])
55
+
56
+    def mvp(self, ent):
57
+        return self.persp_mat() @ self.view_mat() @ ent.translate() @ ent.orient
58
+
59
+    def draw(self, ent, loc_mvp, loc_col):
60
+        if not ent.vao:
61
+            ent.build_vao()
62
+        glUniformMatrix4fv(loc_mvp, 1, GL_TRUE, self.mvp(ent))
63
+        glUniform4fv(loc_col, 1, ent.col)
64
+        glBindVertexArray(ent.vao)
65
+        glDrawArrays(ent.draw_mode, 0, len(ent.verts))
66
+
67
+    def target(self, scene):
68
+        l = self.looking()
69
+        d, c = None, None
70
+        for ent in scene:
71
+            if ent.draw_mode == GL_TRIANGLE_STRIP:
72
+                tl = (ent.verts[i:i + 3] for i in range(len(ent.verts) - 2))
73
+            elif ent.draw_mode == GL_TRIANGLES:
74
+                tl = (ent.verts[3 * i:3 * (i + 1)] for i in range(len(ent.verts) // 3))
75
+            else:
76
+                raise NotImplementedError
77
+            for t in tl:
78
+                r = intersect_tri(self.pos, l, ent.pos + np.dot(ent.orient[:3, :3].T, t))
79
+                if r and (d is None or r < d):
80
+                    d, c = r, ent
81
+        return c
... ...
@@ -0,0 +1,39 @@
1
+from libs import *
2
+
3
+class Entity:
4
+    def __init__(self, verts, col, pos, mode=GL_TRIANGLE_STRIP):
5
+        self.verts = vec(verts)
6
+        self.col = vec(col)
7
+        self.pos = vec(pos)
8
+        self.draw_mode = mode
9
+        self.orient = np.identity(4, dtype=FLOAT)
10
+        self.vao = None
11
+        self.onclick = lambda s, b: print(f"clicked {s} with button {b}")
12
+
13
+    def build_vao(self):
14
+        self.vao = glGenVertexArrays(1)
15
+        glBindVertexArray(self.vao)
16
+        vbo = glGenBuffers(1)
17
+        glBindBuffer(GL_ARRAY_BUFFER, vbo)
18
+        glBufferData(GL_ARRAY_BUFFER, self.verts.nbytes, self.verts, GL_STATIC_DRAW)
19
+        glEnableVertexAttribArray(0)
20
+        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, None)
21
+
22
+    def translate(self):
23
+        m = np.identity(4, dtype=FLOAT)
24
+        m[:3, 3] = self.pos
25
+        return m
26
+
27
+    def rotate(self, theta, axis):
28
+        x, y, z = axis
29
+        c, s = np.cos(theta), np.sin(theta)
30
+        t = 1 - c
31
+        self.orient = vec([
32
+            [t*x*x + c,   t*x*y - s*z, t*x*z + s*y, 0],
33
+            [t*x*y + s*z, t*y*y + c,   t*y*z - s*x, 0],
34
+            [t*x*z - s*y, t*y*z + s*x, t*z*z + c,   0],
35
+            [0, 0, 0, 1]
36
+        ]) @ self.orient
37
+
38
+    def click(self, button):
39
+        self.onclick(self, button)
... ...
@@ -0,0 +1,12 @@
1
+from OpenGL.GL import *
2
+import numpy as np
3
+import pygame
4
+
5
+FLOAT = np.float32
6
+
7
+def vec(l):
8
+    return np.array(l, dtype=FLOAT)
9
+
10
+def normalize(v):
11
+    norm = np.linalg.norm(v)
12
+    return v / norm if norm > 0 else v
... ...
@@ -0,0 +1,80 @@
1
+from OpenGL.GL import shaders
2
+from libs import *
3
+from player import Player
4
+from entity import Entity
5
+
6
+VS = """
7
+#version 120
8
+attribute vec3 position;
9
+uniform mat4 MVP;
10
+void main() {
11
+    gl_Position = MVP * vec4(position, 1.0);
12
+}
13
+"""
14
+
15
+FS = """
16
+#version 120
17
+uniform vec4 matCol;
18
+void main() {
19
+    gl_FragColor = matCol;
20
+}
21
+"""
22
+
23
+def weaken(self, button):
24
+    if button == 1:
25
+        self.col *= 0.9
26
+
27
+def toggle_spin(self, button):
28
+    if button == 3:
29
+        self.spin = not self.spin
30
+
31
+def build_scene():
32
+    ot = Entity(
33
+        [[-0.5, -0.5, 0.0], [0.5, -0.5, 0.0], [0.0, 0.5, 0.0]],
34
+        [1.0, 0.5, 0.2, 1.0],
35
+        [0.0, 0.0, 3.0]
36
+    )
37
+    ot.spin = True
38
+    ot.onclick = toggle_spin
39
+    cr = Entity(
40
+        [[-0.8, -0.4, 0.0], [-0.8, 0.4, 0.0], [0.8, -0.4, 0.0], [0.8, 0.4, 0.0]],
41
+        [0.2, 0.5, 1.0, 1.0],
42
+        [3.0, -0.5, 0.0]
43
+    )
44
+    cr.rotate(np.radians(90), [0.0, 1.0, 0.0])
45
+    cr.onclick = weaken
46
+    return [ot, cr]
47
+
48
+def main():
49
+    pygame.init()
50
+    pygame.display.set_mode((640, 480), pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
51
+    pygame.display.set_caption("OpenGL!")
52
+    sp = shaders.compileProgram(
53
+        shaders.compileShader(VS, GL_VERTEX_SHADER),
54
+        shaders.compileShader(FS, GL_FRAGMENT_SHADER)
55
+    )
56
+    glUseProgram(sp)
57
+    glEnable(GL_DEPTH_TEST)
58
+    loc_mvp = glGetUniformLocation(sp, "MVP")
59
+    loc_matcol = glGetUniformLocation(sp, "matCol")
60
+    glClearColor(0.1, 0.1, 0.1, 1.0)
61
+    scene = build_scene()
62
+    clock = pygame.time.Clock()
63
+    cam = Player()
64
+    cam.set_focus(True)
65
+    while True:
66
+        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
67
+        for ent in scene:
68
+            cam.draw(ent, loc_mvp, loc_matcol)
69
+        for ev in pygame.event.get():
70
+            if ev.type == pygame.QUIT or ev.type == pygame.KEYUP and ev.unicode == "q":
71
+                pygame.quit()
72
+                return
73
+            cam.handle_event(ev, scene)
74
+        cam.update_pos()
75
+        if scene[0].spin:
76
+            scene[0].rotate(0.03 / np.linalg.norm(scene[0].pos - cam.pos) ** 2, [0.0, 1.0, 0.0])
77
+        pygame.display.flip()
78
+        clock.tick(60)
79
+
80
+main()
... ...
@@ -0,0 +1,59 @@
1
+from libs import *
2
+import camera
3
+
4
+ARROWS = [pygame.K_UP, pygame.K_DOWN, pygame.K_LEFT, pygame.K_RIGHT]
5
+
6
+def reshape(w, h):
7
+    glViewport(0, 0, w, h)
8
+    return w / h
9
+
10
+class Player(camera.Camera):
11
+    def __init__(self, **kwargs):
12
+        super().__init__(**kwargs)
13
+        self.sensitivity = kwargs.get("sensitivity", 0.005)
14
+        self.rv = vec([0, 0, 0])
15
+        self.speed = kwargs.get("speed", 0.05)
16
+
17
+    def set_focus(self, f): 
18
+        pygame.event.set_grab(f)
19
+        pygame.mouse.set_visible(not f)
20
+
21
+    def get_focus(self):
22
+        return not pygame.mouse.get_visible()
23
+
24
+    def mouse_move(self, dx, dy):
25
+        self.theta = (self.theta - self.sensitivity * dx) % (2 * np.pi)
26
+        self.phi = min(np.pi / 2, max(-np.pi / 2, self.phi + self.sensitivity * dy))
27
+                
28
+    def update_motion(self, pk):
29
+        self.rv[0] = pk[pygame.K_RIGHT] - pk[pygame.K_LEFT]
30
+        self.rv[2] = pk[pygame.K_UP] - pk[pygame.K_DOWN]
31
+
32
+    def vdof(self):
33
+        l = self.looking()
34
+        l[1] = 0
35
+        u = self.up()
36
+        return vec([np.cross(l, u), u, l])
37
+
38
+    def update_pos(self):
39
+        self.pos += self.speed * self.vdof() @ self.rv.T
40
+
41
+    def handle_event(self, ev, scene):
42
+        match ev.type:
43
+            case pygame.KEYUP | pygame.KEYDOWN:
44
+                if ev.unicode == "\x1b" and ev.type == pygame.KEYUP:
45
+                    self.set_focus(False)
46
+                elif ev.key in ARROWS:
47
+                    self.update_motion(pygame.key.get_pressed())
48
+            case pygame.MOUSEMOTION:
49
+                if self.get_focus():
50
+                    self.mouse_move(ev.rel[0], -ev.rel[1])
51
+            case pygame.MOUSEBUTTONUP:
52
+                if ev.button == 1:
53
+                    self.set_focus(True)
54
+            case pygame.MOUSEBUTTONDOWN:
55
+                t = self.target(scene)
56
+                if t:
57
+                    t.onclick(ev.button)
58
+            case pygame.WINDOWSIZECHANGED | pygame.WINDOWRESIZED:
59
+                self.aspect = reshape(ev.x, ev.y)
0 60