Minecraft3193092 commited on
Commit
f92c150
1 Parent(s): 55c258d

Upload 15 files

Browse files
Files changed (15) hide show
  1. block_type.py +71 -0
  2. chunk.py +183 -0
  3. collider.py +67 -0
  4. entity.py +161 -0
  5. frag.glsl +18 -0
  6. hit.py +105 -0
  7. main.py +204 -0
  8. matrix.py +139 -0
  9. player.py +78 -0
  10. save.py +99 -0
  11. shader.py +71 -0
  12. subchunk.py +99 -0
  13. texture_manager.py +42 -0
  14. vert.glsl +18 -0
  15. world.py +176 -0
block_type.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import collider
2
+
3
+ import models.cube # default model
4
+
5
+ class Block_type:
6
+ # new optional model argument (cube model by default)
7
+ def __init__(self, texture_manager, name = "unknown", block_face_textures = {"all": "cobblestone"}, model = models.cube):
8
+ self.name = name
9
+ self.block_face_textures = block_face_textures
10
+ self.model = model
11
+
12
+ # create members based on model attributes
13
+
14
+ self.transparent = model.transparent
15
+ self.is_cube = model.is_cube
16
+ self.glass = model.glass
17
+
18
+ # create colliders
19
+
20
+ self.colliders = []
21
+
22
+ for _collider in model.colliders:
23
+ self.colliders.append(collider.Collider(*_collider))
24
+
25
+ # replace data contained in numbers.py with model specific data
26
+
27
+ self.vertex_positions = model.vertex_positions
28
+ self.tex_coords = model.tex_coords.copy()
29
+ self.shading_values = model.shading_values
30
+
31
+ def set_block_face(face, texture):
32
+ # make sure we don't add inexistent faces
33
+
34
+ if face > len(self.tex_coords) - 1:
35
+ return
36
+
37
+ self.tex_coords[face] = self.tex_coords[face].copy()
38
+
39
+ for vertex in range(4):
40
+ self.tex_coords[face][vertex * 3 + 2] = texture
41
+
42
+ for face in block_face_textures:
43
+ texture = block_face_textures[face]
44
+ texture_manager.add_texture(texture)
45
+
46
+ texture_index = texture_manager.textures.index(texture)
47
+
48
+ if face == "all":
49
+ for i in range(len(self.tex_coords)):
50
+ set_block_face(i, texture_index)
51
+
52
+ elif face == "sides":
53
+ set_block_face(0, texture_index)
54
+ set_block_face(1, texture_index)
55
+ set_block_face(4, texture_index)
56
+ set_block_face(5, texture_index)
57
+
58
+ elif face == "x":
59
+ set_block_face(0, texture_index)
60
+ set_block_face(1, texture_index)
61
+
62
+ elif face == "y":
63
+ set_block_face(2, texture_index)
64
+ set_block_face(3, texture_index)
65
+
66
+ elif face == "z":
67
+ set_block_face(4, texture_index)
68
+ set_block_face(5, texture_index)
69
+
70
+ else:
71
+ set_block_face(["right", "left", "top", "bottom", "front", "back"].index(face), texture_index)
chunk.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import math
3
+
4
+ import pyglet.gl as gl
5
+
6
+ import subchunk
7
+
8
+ CHUNK_WIDTH = 16
9
+ CHUNK_HEIGHT = 128
10
+ CHUNK_LENGTH = 16
11
+
12
+ class Chunk:
13
+ def __init__(self, world, chunk_position):
14
+ self.world = world
15
+
16
+ self.modified = False
17
+ self.chunk_position = chunk_position
18
+
19
+ self.position = (
20
+ self.chunk_position[0] * CHUNK_WIDTH,
21
+ self.chunk_position[1] * CHUNK_HEIGHT,
22
+ self.chunk_position[2] * CHUNK_LENGTH)
23
+
24
+ self.blocks = [[[0
25
+ for z in range(CHUNK_LENGTH)]
26
+ for y in range(CHUNK_HEIGHT)]
27
+ for x in range(CHUNK_WIDTH )]
28
+
29
+ self.subchunks = {}
30
+
31
+ for x in range(int(CHUNK_WIDTH / subchunk.SUBCHUNK_WIDTH)):
32
+ for y in range(int(CHUNK_HEIGHT / subchunk.SUBCHUNK_HEIGHT)):
33
+ for z in range(int(CHUNK_LENGTH / subchunk.SUBCHUNK_LENGTH)):
34
+ self.subchunks[(x, y, z)] = subchunk.Subchunk(self, (x, y, z))
35
+
36
+ # mesh variables
37
+
38
+ self.mesh_vertex_positions = []
39
+ self.mesh_tex_coords = []
40
+ self.mesh_shading_values = []
41
+
42
+ self.mesh_index_counter = 0
43
+ self.mesh_indices = []
44
+
45
+ # create VAO and VBO's
46
+
47
+ self.vao = gl.GLuint(0)
48
+ gl.glGenVertexArrays(1, self.vao)
49
+ gl.glBindVertexArray(self.vao)
50
+
51
+ self.vertex_position_vbo = gl.GLuint(0)
52
+ gl.glGenBuffers(1, self.vertex_position_vbo)
53
+
54
+ self.tex_coord_vbo = gl.GLuint(0)
55
+ gl.glGenBuffers(1, self.tex_coord_vbo)
56
+
57
+ self.shading_values_vbo = gl.GLuint(0)
58
+ gl.glGenBuffers(1, self.shading_values_vbo)
59
+
60
+ self.ibo = gl.GLuint(0)
61
+ gl.glGenBuffers(1, self.ibo)
62
+
63
+ def update_subchunk_meshes(self):
64
+ for subchunk_position in self.subchunks:
65
+ subchunk = self.subchunks[subchunk_position]
66
+ subchunk.update_mesh()
67
+
68
+ def update_at_position(self, position):
69
+ x, y, z = position
70
+
71
+ lx = int(x % subchunk.SUBCHUNK_WIDTH )
72
+ ly = int(y % subchunk.SUBCHUNK_HEIGHT)
73
+ lz = int(z % subchunk.SUBCHUNK_LENGTH)
74
+
75
+ clx, cly, clz = self.world.get_local_position(position)
76
+
77
+ sx = math.floor(clx / subchunk.SUBCHUNK_WIDTH)
78
+ sy = math.floor(cly / subchunk.SUBCHUNK_HEIGHT)
79
+ sz = math.floor(clz / subchunk.SUBCHUNK_LENGTH)
80
+
81
+ self.subchunks[(sx, sy, sz)].update_mesh()
82
+
83
+ def try_update_subchunk_mesh(subchunk_position):
84
+ if subchunk_position in self.subchunks:
85
+ self.subchunks[subchunk_position].update_mesh()
86
+
87
+ if lx == subchunk.SUBCHUNK_WIDTH - 1: try_update_subchunk_mesh((sx + 1, sy, sz))
88
+ if lx == 0: try_update_subchunk_mesh((sx - 1, sy, sz))
89
+
90
+ if ly == subchunk.SUBCHUNK_HEIGHT - 1: try_update_subchunk_mesh((sx, sy + 1, sz))
91
+ if ly == 0: try_update_subchunk_mesh((sx, sy - 1, sz))
92
+
93
+ if lz == subchunk.SUBCHUNK_LENGTH - 1: try_update_subchunk_mesh((sx, sy, sz + 1))
94
+ if lz == 0: try_update_subchunk_mesh((sx, sy, sz - 1))
95
+
96
+ def update_mesh(self):
97
+ # combine all the small subchunk meshes into one big chunk mesh
98
+
99
+ self.mesh_vertex_positions = []
100
+ self.mesh_tex_coords = []
101
+ self.mesh_shading_values = []
102
+
103
+ self.mesh_index_counter = 0
104
+ self.mesh_indices = []
105
+
106
+ for subchunk_position in self.subchunks:
107
+ subchunk = self.subchunks[subchunk_position]
108
+
109
+ self.mesh_vertex_positions.extend(subchunk.mesh_vertex_positions)
110
+ self.mesh_tex_coords.extend(subchunk.mesh_tex_coords)
111
+ self.mesh_shading_values.extend(subchunk.mesh_shading_values)
112
+
113
+ mesh_indices = [index + self.mesh_index_counter for index in subchunk.mesh_indices]
114
+
115
+ self.mesh_indices.extend(mesh_indices)
116
+ self.mesh_index_counter += subchunk.mesh_index_counter
117
+
118
+ # send the full mesh data to the GPU and free the memory used client-side (we don't need it anymore)
119
+ # don't forget to save the length of 'self.mesh_indices' before freeing
120
+
121
+ self.mesh_indices_length = len(self.mesh_indices)
122
+ self.send_mesh_data_to_gpu()
123
+
124
+ del self.mesh_vertex_positions
125
+ del self.mesh_tex_coords
126
+ del self.mesh_shading_values
127
+
128
+ del self.mesh_indices
129
+
130
+ def send_mesh_data_to_gpu(self): # pass mesh data to gpu
131
+ if not self.mesh_index_counter:
132
+ return
133
+
134
+ gl.glBindVertexArray(self.vao)
135
+
136
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertex_position_vbo)
137
+ gl.glBufferData(
138
+ gl.GL_ARRAY_BUFFER,
139
+ ctypes.sizeof(gl.GLfloat * len(self.mesh_vertex_positions)),
140
+ (gl.GLfloat * len(self.mesh_vertex_positions)) (*self.mesh_vertex_positions),
141
+ gl.GL_STATIC_DRAW)
142
+
143
+ gl.glVertexAttribPointer(0, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, 0)
144
+ gl.glEnableVertexAttribArray(0)
145
+
146
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.tex_coord_vbo)
147
+ gl.glBufferData(
148
+ gl.GL_ARRAY_BUFFER,
149
+ ctypes.sizeof(gl.GLfloat * len(self.mesh_tex_coords)),
150
+ (gl.GLfloat * len(self.mesh_tex_coords)) (*self.mesh_tex_coords),
151
+ gl.GL_STATIC_DRAW)
152
+
153
+ gl.glVertexAttribPointer(1, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, 0)
154
+ gl.glEnableVertexAttribArray(1)
155
+
156
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.shading_values_vbo)
157
+ gl.glBufferData(
158
+ gl.GL_ARRAY_BUFFER,
159
+ ctypes.sizeof(gl.GLfloat * len(self.mesh_shading_values)),
160
+ (gl.GLfloat * len(self.mesh_shading_values)) (*self.mesh_shading_values),
161
+ gl.GL_STATIC_DRAW)
162
+
163
+ gl.glVertexAttribPointer(2, 1, gl.GL_FLOAT, gl.GL_FALSE, 0, 0)
164
+ gl.glEnableVertexAttribArray(2)
165
+
166
+ gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.ibo)
167
+ gl.glBufferData(
168
+ gl.GL_ELEMENT_ARRAY_BUFFER,
169
+ ctypes.sizeof(gl.GLuint * self.mesh_indices_length),
170
+ (gl.GLuint * self.mesh_indices_length) (*self.mesh_indices),
171
+ gl.GL_STATIC_DRAW)
172
+
173
+ def draw(self):
174
+ if not self.mesh_index_counter:
175
+ return
176
+
177
+ gl.glBindVertexArray(self.vao)
178
+
179
+ gl.glDrawElements(
180
+ gl.GL_TRIANGLES,
181
+ self.mesh_indices_length,
182
+ gl.GL_UNSIGNED_INT,
183
+ None)
collider.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Collider:
2
+ def __init__(self, pos1 = (None,) * 3, pos2 = (None,) * 3):
3
+ # pos1: position of the collider vertex in the -X, -Y, -Z direction
4
+ # pos2: position of the collider vertex in the +X, +Y, +Z direction
5
+
6
+ self.x1, self.y1, self.z1 = pos1
7
+ self.x2, self.y2, self.z2 = pos2
8
+
9
+ def __add__(self, pos):
10
+ x, y, z = pos
11
+
12
+ return Collider(
13
+ (self.x1 + x, self.y1 + y, self.z1 + z),
14
+ (self.x2 + x, self.y2 + y, self.z2 + z)
15
+ )
16
+
17
+ def __and__(self, collider):
18
+ x = min(self.x2, collider.x2) - max(self.x1, collider.x1)
19
+ y = min(self.y2, collider.y2) - max(self.y1, collider.y1)
20
+ z = min(self.z2, collider.z2) - max(self.z1, collider.z1)
21
+
22
+ return x > 0 and y > 0 and z > 0
23
+
24
+ def collide(self, collider, velocity):
25
+ # self: the dynamic collider, which moves
26
+ # collider: the static collider, which stays put
27
+
28
+ no_collision = 1, None
29
+
30
+ # find entry & exit times for each axis
31
+
32
+ vx, vy, vz = velocity
33
+
34
+ time = lambda x, y: x / y if y else float('-' * (x > 0) + "inf")
35
+
36
+ x_entry = time(collider.x1 - self.x2 if vx > 0 else collider.x2 - self.x1, vx)
37
+ x_exit = time(collider.x2 - self.x1 if vx > 0 else collider.x1 - self.x2, vx)
38
+
39
+ y_entry = time(collider.y1 - self.y2 if vy > 0 else collider.y2 - self.y1, vy)
40
+ y_exit = time(collider.y2 - self.y1 if vy > 0 else collider.y1 - self.y2, vy)
41
+
42
+ z_entry = time(collider.z1 - self.z2 if vz > 0 else collider.z2 - self.z1, vz)
43
+ z_exit = time(collider.z2 - self.z1 if vz > 0 else collider.z1 - self.z2, vz)
44
+
45
+ # make sure we actually got a collision
46
+
47
+ if x_entry < 0 and y_entry < 0 and z_entry < 0:
48
+ return no_collision
49
+
50
+ if x_entry > 1 or y_entry > 1 or z_entry > 1:
51
+ return no_collision
52
+
53
+ # on which axis did we collide first?
54
+
55
+ entry = max(x_entry, y_entry, z_entry)
56
+ exit_ = min(x_exit, y_exit, z_exit )
57
+
58
+ if entry > exit_:
59
+ return no_collision
60
+
61
+ # find normal of surface we collided with
62
+
63
+ nx = (0, -1 if vx > 0 else 1)[entry == x_entry]
64
+ ny = (0, -1 if vy > 0 else 1)[entry == y_entry]
65
+ nz = (0, -1 if vz > 0 else 1)[entry == z_entry]
66
+
67
+ return entry, (nx, ny, nz)
entity.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import collider
3
+
4
+ FLYING_ACCEL = (0, 0, 0)
5
+ GRAVITY_ACCEL = (0, -32, 0)
6
+
7
+ # these values all come (losely) from Minecraft, but are multiplied by 20 (since Minecraft runs at 20 TPS)
8
+
9
+ FRICTION = ( 20, 20, 20)
10
+
11
+ DRAG_FLY = ( 5, 5, 5)
12
+ DRAG_JUMP = (1.8, 0, 1.8)
13
+ DRAG_FALL = (1.8, 0.4, 1.8)
14
+
15
+ class Entity:
16
+ def __init__(self, world):
17
+ self.world = world
18
+
19
+ # physical variables
20
+
21
+ self.jump_height = 1.25
22
+ self.flying = False
23
+
24
+ self.position = [0, 80, 0]
25
+ self.rotation = [-math.tau / 4, 0]
26
+
27
+ self.velocity = [0, 0, 0]
28
+ self.accel = [0, 0, 0]
29
+
30
+ # collision variables
31
+
32
+ self.width = 0.6
33
+ self.height = 1.8
34
+
35
+ self.collider = collider.Collider()
36
+ self.grounded = False
37
+
38
+ def update_collider(self):
39
+ x, y, z = self.position
40
+
41
+ self.collider.x1 = x - self.width / 2
42
+ self.collider.x2 = x + self.width / 2
43
+
44
+ self.collider.y1 = y
45
+ self.collider.y2 = y + self.height
46
+
47
+ self.collider.z1 = z - self.width / 2
48
+ self.collider.z2 = z + self.width / 2
49
+
50
+ def teleport(self, pos):
51
+ self.position = list(pos)
52
+ self.velocity = [0, 0, 0] # to prevent collisions
53
+
54
+ def jump(self, height = None):
55
+ # obviously, we can't initiate a jump while in mid-air
56
+
57
+ if not self.grounded:
58
+ return
59
+
60
+ if height is None:
61
+ height = self.jump_height
62
+
63
+ self.velocity[1] = math.sqrt(-2 * GRAVITY_ACCEL[1] * height)
64
+
65
+ @property
66
+ def friction(self):
67
+ if self.flying:
68
+ return DRAG_FLY
69
+
70
+ elif self.grounded:
71
+ return FRICTION
72
+
73
+ elif self.velocity[1] > 0:
74
+ return DRAG_JUMP
75
+
76
+ return DRAG_FALL
77
+
78
+ def update(self, delta_time):
79
+ # apply input acceleration, and adjust for friction/drag
80
+
81
+ self.velocity = [v + a * f * delta_time for v, a, f in zip(self.velocity, self.accel, self.friction)]
82
+ self.accel = [0, 0, 0]
83
+
84
+ # compute collisions
85
+
86
+ self.update_collider()
87
+ self.grounded = False
88
+
89
+ for _ in range(3):
90
+ adjusted_velocity = [v * delta_time for v in self.velocity]
91
+ vx, vy, vz = adjusted_velocity
92
+
93
+ # find all the blocks we could potentially be colliding with
94
+ # this step is known as "broad-phasing"
95
+
96
+ step_x = 1 if vx > 0 else -1
97
+ step_y = 1 if vy > 0 else -1
98
+ step_z = 1 if vz > 0 else -1
99
+
100
+ steps_xz = int(self.width / 2)
101
+ steps_y = int(self.height)
102
+
103
+ x, y, z = map(int, self.position)
104
+ cx, cy, cz = [int(x + v) for x, v in zip(self.position, adjusted_velocity)]
105
+
106
+ potential_collisions = []
107
+
108
+ for i in range(x - step_x * (steps_xz + 1), cx + step_x * (steps_xz + 2), step_x):
109
+ for j in range(y - step_y * (steps_y + 2), cy + step_y * (steps_y + 3), step_y):
110
+ for k in range(z - step_z * (steps_xz + 1), cz + step_z * (steps_xz + 2), step_z):
111
+ pos = (i, j, k)
112
+ num = self.world.get_block_number(pos)
113
+
114
+ if not num:
115
+ continue
116
+
117
+ for _collider in self.world.block_types[num].colliders:
118
+ entry_time, normal = self.collider.collide(_collider + pos, adjusted_velocity)
119
+
120
+ if normal is None:
121
+ continue
122
+
123
+ potential_collisions.append((entry_time, normal))
124
+
125
+ # get first collision
126
+
127
+ if not potential_collisions:
128
+ break
129
+
130
+ entry_time, normal = min(potential_collisions, key = lambda x: x[0])
131
+ entry_time -= 0.001
132
+
133
+ if normal[0]:
134
+ self.velocity[0] = 0
135
+ self.position[0] += vx * entry_time
136
+
137
+ if normal[1]:
138
+ self.velocity[1] = 0
139
+ self.position[1] += vy * entry_time
140
+
141
+ if normal[2]:
142
+ self.velocity[2] = 0
143
+ self.position[2] += vz * entry_time
144
+
145
+ if normal[1] == 1:
146
+ self.grounded = True
147
+
148
+ self.position = [x + v * delta_time for x, v in zip(self.position, self.velocity)]
149
+
150
+ # apply gravity acceleration
151
+
152
+ gravity = FLYING_ACCEL if self.flying else GRAVITY_ACCEL
153
+ self.velocity = [v + a * delta_time for v, a in zip(self.velocity, gravity)]
154
+
155
+ # apply friction/drag
156
+
157
+ self.velocity = [v - min(v * f * delta_time, v, key = abs) for v, f in zip(self.velocity, self.friction)]
158
+
159
+ # make sure we can rely on the entity's collider outside of this function
160
+
161
+ self.update_collider()
frag.glsl ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 330
2
+
3
+ out vec4 fragment_colour;
4
+
5
+ uniform sampler2DArray texture_array_sampler;
6
+
7
+ in vec3 local_position;
8
+ in vec3 interpolated_tex_coords;
9
+ in float interpolated_shading_value;
10
+
11
+ void main(void) {
12
+ vec4 texture_colour = texture(texture_array_sampler, interpolated_tex_coords);
13
+ fragment_colour = texture_colour * interpolated_shading_value;
14
+
15
+ if (texture_colour.a == 0.0) { // discard if texel's alpha component is 0 (texel is transparent)
16
+ discard;
17
+ }
18
+ }
hit.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ HIT_RANGE = 3
4
+
5
+ class Hit_ray:
6
+ def __init__(self, world, rotation, starting_position):
7
+ self.world = world
8
+
9
+ # get the ray unit vector based on rotation angles
10
+ # sqrt(ux ^ 2 + uy ^ 2 + uz ^ 2) must always equal 1
11
+
12
+ self.vector = (
13
+ math.cos(rotation[0]) * math.cos(rotation[1]),
14
+ math.sin(rotation[1]),
15
+ math.sin(rotation[0]) * math.cos(rotation[1]))
16
+
17
+ # point position
18
+ self.position = list(starting_position)
19
+
20
+ # block position in which point currently is
21
+ self.block = tuple(map(lambda x: int(round(x)), self.position))
22
+
23
+ # current distance the point has travelled
24
+ self.distance = 0
25
+
26
+ # 'check' and 'step' both return 'True' if something is hit, and 'False' if not
27
+
28
+ def check(self, hit_callback, distance, current_block, next_block):
29
+ if self.world.get_block_number(next_block):
30
+ hit_callback(current_block, next_block)
31
+ return True
32
+
33
+ else:
34
+ self.position = list(map(lambda x: self.position[x] + self.vector[x] * distance, range(3)))
35
+
36
+ self.block = next_block
37
+ self.distance += distance
38
+
39
+ return False
40
+
41
+ def step(self, hit_callback):
42
+ bx, by, bz = self.block
43
+
44
+ # point position relative to block centre
45
+ local_position = list(map(lambda x: self.position[x] - self.block[x], range(3)))
46
+
47
+ # we don't want to deal with negatives, so remove the sign
48
+ # this is also cool because it means we don't need to take into account the sign of our ray vector
49
+ # we do need to remember which components were negative for later on, however
50
+
51
+ sign = [1, 1, 1] # '1' for positive, '-1' for negative
52
+ absolute_vector = list(self.vector)
53
+
54
+ for component in range(3):
55
+ if self.vector[component] < 0:
56
+ sign[component] = -1
57
+
58
+ absolute_vector[component] = -absolute_vector[component]
59
+ local_position[component] = -local_position[component]
60
+
61
+ lx, ly, lz = local_position
62
+ vx, vy, vz = absolute_vector
63
+
64
+ # calculate intersections
65
+ # I only detail the math for the first component (X) because the rest is pretty self-explanatory
66
+
67
+ # ray line (passing through the point) r ≡ (x - lx) / vx = (y - ly) / lz = (z - lz) / vz (parametric equation)
68
+
69
+ # +x face fx ≡ x = 0.5 (y & z can be any real number)
70
+ # r ∩ fx ≡ (0.5 - lx) / vx = (y - ly) / vy = (z - lz) / vz
71
+
72
+ # x: x = 0.5
73
+ # y: (y - ly) / vy = (0.5 - lx) / vx IFF y = (0.5 - lx) / vx * vy + ly
74
+ # z: (z - lz) / vz = (0.5 - lx) / vx IFF z = (0.5 - lx) / vx * vz + lz
75
+
76
+ if vx:
77
+ x = 0.5
78
+ y = (0.5 - lx) / vx * vy + ly
79
+ z = (0.5 - lx) / vx * vz + lz
80
+
81
+ if y >= -0.5 and y <= 0.5 and z >= -0.5 and z <= 0.5:
82
+ distance = math.sqrt((x - lx) ** 2 + (y - ly) ** 2 + (z - lz) ** 2)
83
+
84
+ # we can return straight away here
85
+ # if we intersect with one face, we know for a fact we're not intersecting with any of the others
86
+
87
+ return self.check(hit_callback, distance, (bx, by, bz), (bx + sign[0], by, bz))
88
+
89
+ if vy:
90
+ x = (0.5 - ly) / vy * vx + lx
91
+ y = 0.5
92
+ z = (0.5 - ly) / vy * vz + lz
93
+
94
+ if x >= -0.5 and x <= 0.5 and z >= -0.5 and z <= 0.5:
95
+ distance = math.sqrt((x - lx) ** 2 + (y - ly) ** 2 + (z - lz) ** 2)
96
+ return self.check(hit_callback, distance, (bx, by, bz), (bx, by + sign[1], bz))
97
+
98
+ if vz:
99
+ x = (0.5 - lz) / vz * vx + lx
100
+ y = (0.5 - lz) / vz * vy + ly
101
+ z = 0.5
102
+
103
+ if x >= -0.5 and x <= 0.5 and y >= -0.5 and y <= 0.5:
104
+ distance = math.sqrt((x - lx) ** 2 + (y - ly) ** 2 + (z - lz) ** 2)
105
+ return self.check(hit_callback, distance, (bx, by, bz), (bx, by, bz + sign[2]))
main.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import ctypes
3
+ import random
4
+ import pyglet
5
+
6
+ pyglet.options["shadow_window"] = False
7
+ pyglet.options["debug_gl"] = False
8
+
9
+ import pyglet.gl as gl
10
+
11
+ import matrix
12
+ import shader
13
+ import player
14
+
15
+ import block_type
16
+ import texture_manager
17
+
18
+ import chunk
19
+ import world
20
+
21
+ import hit
22
+
23
+ class Window(pyglet.window.Window):
24
+ def __init__(self, **args):
25
+ super().__init__(**args)
26
+
27
+ # create world
28
+
29
+ self.world = world.World()
30
+
31
+ # create shader
32
+
33
+ self.shader = shader.Shader("vert.glsl", "frag.glsl")
34
+ self.shader_sampler_location = self.shader.find_uniform(b"texture_array_sampler")
35
+ self.shader.use()
36
+
37
+ # pyglet stuff
38
+
39
+ pyglet.clock.schedule_interval(self.update, 1.0 / 60)
40
+ self.mouse_captured = False
41
+
42
+ # player stuff
43
+
44
+ self.player = player.Player(self.world, self.shader, self.width, self.height)
45
+
46
+ # misc stuff
47
+
48
+ self.holding = 44 # 5
49
+
50
+ def update(self, delta_time):
51
+ # print(f"FPS: {1.0 / delta_time}")
52
+
53
+ if not self.mouse_captured:
54
+ self.player.input = [0, 0, 0]
55
+
56
+ self.player.update(delta_time)
57
+
58
+ def on_draw(self):
59
+ self.player.update_matrices()
60
+
61
+ # bind textures
62
+
63
+ gl.glActiveTexture(gl.GL_TEXTURE0)
64
+ gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.world.texture_manager.texture_array)
65
+ gl.glUniform1i(self.shader_sampler_location, 0)
66
+
67
+ # draw stuff
68
+
69
+ gl.glEnable(gl.GL_DEPTH_TEST)
70
+ gl.glEnable(gl.GL_CULL_FACE)
71
+
72
+ gl.glClearColor(0.0, 0.0, 0.0, 0.0)
73
+ self.clear()
74
+ self.world.draw()
75
+
76
+ gl.glFinish()
77
+
78
+ # input functions
79
+
80
+ def on_resize(self, width, height):
81
+ print(f"Resize {width} * {height}")
82
+ gl.glViewport(0, 0, width, height)
83
+
84
+ self.player.view_width = width
85
+ self.player.view_height = height
86
+
87
+ def on_mouse_press(self, x, y, button, modifiers):
88
+ if not self.mouse_captured:
89
+ self.mouse_captured = True
90
+ self.set_exclusive_mouse(True)
91
+
92
+ return
93
+
94
+ # handle breaking/placing blocks
95
+
96
+ def hit_callback(current_block, next_block):
97
+ if button == pyglet.window.mouse.RIGHT: self.world.try_set_block(current_block, self.holding, self.player.collider)
98
+ elif button == pyglet.window.mouse.LEFT: self.world.set_block(next_block, 0)
99
+ elif button == pyglet.window.mouse.MIDDLE: self.holding = self.world.get_block_number(next_block)
100
+
101
+ x, y, z = self.player.position
102
+ y += self.player.eyelevel
103
+
104
+ hit_ray = hit.Hit_ray(self.world, self.player.rotation, (x, y, z))
105
+
106
+ while hit_ray.distance < hit.HIT_RANGE:
107
+ if hit_ray.step(hit_callback):
108
+ break
109
+
110
+ def on_mouse_motion(self, x, y, delta_x, delta_y):
111
+ if self.mouse_captured:
112
+ sensitivity = 0.004
113
+
114
+ self.player.rotation[0] += delta_x * sensitivity
115
+ self.player.rotation[1] += delta_y * sensitivity
116
+
117
+ self.player.rotation[1] = max(-math.tau / 4, min(math.tau / 4, self.player.rotation[1]))
118
+
119
+ def on_mouse_drag(self, x, y, delta_x, delta_y, buttons, modifiers):
120
+ self.on_mouse_motion(x, y, delta_x, delta_y)
121
+
122
+ def on_key_press(self, key, modifiers):
123
+ if not self.mouse_captured:
124
+ return
125
+
126
+ if key == pyglet.window.key.D: self.player.input[0] += 1
127
+ elif key == pyglet.window.key.A: self.player.input[0] -= 1
128
+ elif key == pyglet.window.key.W: self.player.input[2] += 1
129
+ elif key == pyglet.window.key.S: self.player.input[2] -= 1
130
+
131
+ elif key == pyglet.window.key.SPACE : self.player.input[1] += 1
132
+ elif key == pyglet.window.key.LSHIFT: self.player.input[1] -= 1
133
+ elif key == pyglet.window.key.LCTRL : self.player.target_speed = player.SPRINTING_SPEED
134
+
135
+ elif key == pyglet.window.key.F:
136
+ self.player.flying = not self.player.flying
137
+
138
+ elif key == pyglet.window.key.G:
139
+ self.holding = random.randint(1, len(self.world.block_types) - 1)
140
+
141
+ elif key == pyglet.window.key.O:
142
+ self.world.save.save()
143
+
144
+ elif key == pyglet.window.key.R:
145
+ # how large is the world?
146
+
147
+ max_y = 0
148
+
149
+ max_x, max_z = (0, 0)
150
+ min_x, min_z = (0, 0)
151
+
152
+ for pos in self.world.chunks:
153
+ x, y, z = pos
154
+
155
+ max_y = max(max_y, (y + 1) * chunk.CHUNK_HEIGHT)
156
+
157
+ max_x = max(max_x, (x + 1) * chunk.CHUNK_WIDTH)
158
+ min_x = min(min_x, x * chunk.CHUNK_WIDTH)
159
+
160
+ max_z = max(max_z, (z + 1) * chunk.CHUNK_LENGTH)
161
+ min_z = min(min_z, z * chunk.CHUNK_LENGTH)
162
+
163
+ # get random X & Z coordinates to teleport the player to
164
+
165
+ x = random.randint(min_x, max_x)
166
+ z = random.randint(min_z, max_z)
167
+
168
+ # find height at which to teleport to, by finding the first non-air block from the top of the world
169
+
170
+ for y in range(chunk.CHUNK_HEIGHT - 1, -1, -1):
171
+ if not self.world.get_block_number((x, y, z)):
172
+ continue
173
+
174
+ self.player.teleport((x, y + 1, z))
175
+ break
176
+
177
+ elif key == pyglet.window.key.ESCAPE:
178
+ self.mouse_captured = False
179
+ self.set_exclusive_mouse(False)
180
+
181
+ def on_key_release(self, key, modifiers):
182
+ if not self.mouse_captured:
183
+ return
184
+
185
+ if key == pyglet.window.key.D: self.player.input[0] -= 1
186
+ elif key == pyglet.window.key.A: self.player.input[0] += 1
187
+ elif key == pyglet.window.key.W: self.player.input[2] -= 1
188
+ elif key == pyglet.window.key.S: self.player.input[2] += 1
189
+
190
+ elif key == pyglet.window.key.SPACE : self.player.input[1] -= 1
191
+ elif key == pyglet.window.key.LSHIFT: self.player.input[1] += 1
192
+ elif key == pyglet.window.key.LCTRL : self.player.target_speed = player.WALKING_SPEED
193
+
194
+ class Game:
195
+ def __init__(self):
196
+ self.config = gl.Config(double_buffer = True, major_version = 3, minor_version = 3, depth_size = 16)
197
+ self.window = Window(config = self.config, width = 800, height = 600, caption = "Minecraft clone", resizable = True, vsync = False)
198
+
199
+ def run(self):
200
+ pyglet.app.run()
201
+
202
+ if __name__ == "__main__":
203
+ game = Game()
204
+ game.run()
matrix.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import copy
3
+ import ctypes
4
+ import math
5
+
6
+ def copy_matrix(matrix):
7
+ return copy.deepcopy(matrix) # we need to use deepcopy since we're dealing with 2D arrays
8
+
9
+ clean_matrix = [[0.0 for x in range(4)] for x in range(4)]
10
+ identity_matrix = copy_matrix(clean_matrix)
11
+
12
+ identity_matrix[0][0] = 1.0
13
+ identity_matrix[1][1] = 1.0
14
+ identity_matrix[2][2] = 1.0
15
+ identity_matrix[3][3] = 1.0
16
+
17
+ def multiply_matrices(x_matrix, y_matrix):
18
+ result_matrix = copy_matrix(clean_matrix)
19
+
20
+ for i in range(4):
21
+ for j in range(4):
22
+ result_matrix[i][j] = \
23
+ (x_matrix[0][j] * y_matrix[i][0]) + \
24
+ (x_matrix[1][j] * y_matrix[i][1]) + \
25
+ (x_matrix[2][j] * y_matrix[i][2]) + \
26
+ (x_matrix[3][j] * y_matrix[i][3])
27
+
28
+ return result_matrix
29
+
30
+ class Matrix:
31
+ def __init__(self, base = None):
32
+ if type(base) == Matrix: self.data = copy_matrix(base.data)
33
+ elif type(base) == list: self.data = copy_matrix(base)
34
+ else: self.data = copy_matrix(clean_matrix)
35
+
36
+ def load_identity(self):
37
+ self.data = copy_matrix(identity_matrix)
38
+
39
+ def __mul__(self, matrix):
40
+ return Matrix(multiply_matrices(self.data, matrix.data))
41
+
42
+ def __imul__(self, matrix):
43
+ self.data = multiply_matrices(self.data, matrix.data)
44
+
45
+ def scale(self, x, y, z):
46
+ for i in range(4): self.data[0][i] *= x
47
+ for i in range(4): self.data[1][i] *= y
48
+ for i in range(4): self.data[2][i] *= z
49
+
50
+ def translate(self, x, y, z):
51
+ for i in range(4):
52
+ self.data[3][i] = self.data[3][i] + (self.data[0][i] * x + self.data[1][i] * y + self.data[2][i] * z)
53
+
54
+ def rotate(self, angle, x, y, z):
55
+ magnitude = math.sqrt(x * x + y * y + z * z)
56
+
57
+ x /= -magnitude
58
+ y /= -magnitude
59
+ z /= -magnitude
60
+
61
+ sin_angle = math.sin(angle)
62
+ cos_angle = math.cos(angle)
63
+ one_minus_cos = 1.0 - cos_angle
64
+
65
+ xx = x * x
66
+ yy = y * y
67
+ zz = z * z
68
+
69
+ xy = x * y
70
+ yz = y * z
71
+ zx = z * x
72
+
73
+ xs = x * sin_angle
74
+ ys = y * sin_angle
75
+ zs = z * sin_angle
76
+
77
+ rotation_matrix = copy_matrix(clean_matrix)
78
+
79
+ rotation_matrix[0][0] = (one_minus_cos * xx) + cos_angle
80
+ rotation_matrix[0][1] = (one_minus_cos * xy) - zs
81
+ rotation_matrix[0][2] = (one_minus_cos * zx) + ys
82
+
83
+ rotation_matrix[1][0] = (one_minus_cos * xy) + zs
84
+ rotation_matrix[1][1] = (one_minus_cos * yy) + cos_angle
85
+ rotation_matrix[1][2] = (one_minus_cos * yz) - xs
86
+
87
+ rotation_matrix[2][0] = (one_minus_cos * zx) - ys
88
+ rotation_matrix[2][1] = (one_minus_cos * yz) + xs
89
+ rotation_matrix[2][2] = (one_minus_cos * zz) + cos_angle
90
+
91
+ rotation_matrix[3][3] = 1.0
92
+ self.data = multiply_matrices(self.data, rotation_matrix)
93
+
94
+ def rotate_2d(self, x, y):
95
+ self.rotate(x, 0, 1.0, 0)
96
+ self.rotate(-y, math.cos(x), 0, math.sin(x))
97
+
98
+ def frustum(self, left, right, bottom, top, near, far):
99
+ deltax = right - left
100
+ deltay = top - bottom
101
+ deltaz = far - near
102
+
103
+ frustum_matrix = copy_matrix(clean_matrix)
104
+
105
+ frustum_matrix[0][0] = 2 * near / deltax
106
+ frustum_matrix[1][1] = 2 * near / deltay
107
+
108
+ frustum_matrix[2][0] = (right + left) / deltax
109
+ frustum_matrix[2][1] = (top + bottom) / deltay
110
+ frustum_matrix[2][2] = -(near + far) / deltaz
111
+
112
+ frustum_matrix[2][3] = -1.0
113
+ frustum_matrix[3][2] = -2 * near * far / deltaz
114
+
115
+ self.data = multiply_matrices(self.data, frustum_matrix)
116
+
117
+ def perspective(self, fovy, aspect, near, far):
118
+ frustum_y = math.tan(math.radians(fovy) / 2)
119
+ frustum_x = frustum_y * aspect
120
+
121
+ self.frustum(-frustum_x * near, frustum_x * near, -frustum_y * near, frustum_y * near, near, far)
122
+
123
+ def orthographic(self, left, right, bottom, top, near, far):
124
+ deltax = right - left
125
+ deltay = top - bottom
126
+ deltaz = far - near
127
+
128
+ orthographic_matrix = copy_matrix(identity_matrix)
129
+
130
+ orthographic_matrix[0][0] = 2.0 / deltax
131
+ orthographic_matrix[3][0] = -(right + left) / deltax
132
+
133
+ orthographic_matrix[1][1] = 2.0 / deltay
134
+ orthographic_matrix[3][1] = -(top + bottom) / deltay
135
+
136
+ orthographic_matrix[2][2] = 2.0 / deltax
137
+ orthographic_matrix[3][2] = -(near + far) / deltaz
138
+
139
+ self.data = multiply_matrices(self.data, orthographic_matrix)
player.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import entity
3
+ import matrix
4
+
5
+ WALKING_SPEED = 4.317
6
+ SPRINTING_SPEED = 7 # faster than in Minecraft, feels better
7
+
8
+ class Player(entity.Entity):
9
+ def __init__(self, world, shader, width, height):
10
+ super().__init__(world)
11
+
12
+ self.view_width = width
13
+ self.view_height = height
14
+
15
+ # create matrices
16
+
17
+ self.mv_matrix = matrix.Matrix()
18
+ self.p_matrix = matrix.Matrix()
19
+
20
+ # shaders
21
+
22
+ self.shader = shader
23
+ self.shader_matrix_location = self.shader.find_uniform(b"matrix")
24
+
25
+ # camera variables
26
+
27
+ self.eyelevel = self.height - 0.2
28
+ self.input = [0, 0, 0]
29
+
30
+ self.target_speed = WALKING_SPEED
31
+ self.speed = self.target_speed
32
+
33
+ def update(self, delta_time):
34
+ # process input
35
+
36
+ if delta_time * 20 > 1:
37
+ self.speed = self.target_speed
38
+
39
+ else:
40
+ self.speed += (self.target_speed - self.speed) * delta_time * 20
41
+
42
+ multiplier = self.speed * (1, 2)[self.flying]
43
+
44
+ if self.flying and self.input[1]:
45
+ self.accel[1] = self.input[1] * multiplier
46
+
47
+ if self.input[0] or self.input[2]:
48
+ angle = self.rotation[0] - math.atan2(self.input[2], self.input[0]) + math.tau / 4
49
+
50
+ self.accel[0] = math.cos(angle) * multiplier
51
+ self.accel[2] = math.sin(angle) * multiplier
52
+
53
+ if not self.flying and self.input[1] > 0:
54
+ self.jump()
55
+
56
+ # process physics & collisions &c
57
+
58
+ super().update(delta_time)
59
+
60
+ def update_matrices(self):
61
+ # create projection matrix
62
+
63
+ self.p_matrix.load_identity()
64
+
65
+ self.p_matrix.perspective(
66
+ 90 + 10 * (self.speed - WALKING_SPEED) / (SPRINTING_SPEED - WALKING_SPEED),
67
+ float(self.view_width) / self.view_height, 0.1, 500)
68
+
69
+ # create modelview matrix
70
+
71
+ self.mv_matrix.load_identity()
72
+ self.mv_matrix.rotate_2d(self.rotation[0] + math.tau / 4, self.rotation[1])
73
+ self.mv_matrix.translate(-self.position[0], -self.position[1] - self.eyelevel, -self.position[2])
74
+
75
+ # modelviewprojection matrix
76
+
77
+ mvp_matrix = self.p_matrix * self.mv_matrix
78
+ self.shader.uniform_matrix(self.shader_matrix_location, mvp_matrix)
save.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import nbtlib as nbt
2
+ import base36
3
+
4
+ import chunk
5
+
6
+ class Save:
7
+ def __init__(self, world, path = "save"):
8
+ self.world = world
9
+ self.path = path
10
+
11
+ def chunk_position_to_path(self, chunk_position):
12
+ x, y, z = chunk_position
13
+
14
+ chunk_path = '/'.join((self.path,
15
+ base36.dumps(x % 64), base36.dumps(z % 64),
16
+ f"c.{base36.dumps(x)}.{base36.dumps(z)}.dat"))
17
+
18
+ return chunk_path
19
+
20
+ def load_chunk(self, chunk_position):
21
+ # load the chunk file
22
+
23
+ chunk_path = self.chunk_position_to_path(chunk_position)
24
+
25
+ try:
26
+ chunk_blocks = nbt.load(chunk_path)["Level"]["Blocks"]
27
+
28
+ except FileNotFoundError:
29
+ return
30
+
31
+ # create chunk and fill it with the blocks from our chunk file
32
+
33
+ self.world.chunks[chunk_position] = chunk.Chunk(self.world, chunk_position)
34
+
35
+ for x in range(chunk.CHUNK_WIDTH):
36
+ for y in range(chunk.CHUNK_HEIGHT):
37
+ for z in range(chunk.CHUNK_LENGTH):
38
+ self.world.chunks[chunk_position].blocks[x][y][z] = chunk_blocks[
39
+ x * chunk.CHUNK_LENGTH * chunk.CHUNK_HEIGHT +
40
+ z * chunk.CHUNK_HEIGHT +
41
+ y]
42
+
43
+ def save_chunk(self, chunk_position):
44
+ x, y, z = chunk_position
45
+
46
+ # try to load the chunk file
47
+ # if it doesn't exist, create a new one
48
+
49
+ chunk_path = self.chunk_position_to_path(chunk_position)
50
+
51
+ try:
52
+ chunk_data = nbt.load(chunk_path)
53
+
54
+ except FileNotFoundError:
55
+ chunk_data = nbt.File({"": nbt.Compound({"Level": nbt.Compound()})})
56
+
57
+ chunk_data["Level"]["xPos"] = x
58
+ chunk_data["Level"]["zPos"] = z
59
+
60
+ # fill the chunk file with the blocks from our chunk
61
+
62
+ chunk_blocks = nbt.ByteArray([0] * (chunk.CHUNK_WIDTH * chunk.CHUNK_HEIGHT * chunk.CHUNK_LENGTH))
63
+
64
+ for x in range(chunk.CHUNK_WIDTH):
65
+ for y in range(chunk.CHUNK_HEIGHT):
66
+ for z in range(chunk.CHUNK_LENGTH):
67
+ chunk_blocks[
68
+ x * chunk.CHUNK_LENGTH * chunk.CHUNK_HEIGHT +
69
+ z * chunk.CHUNK_HEIGHT +
70
+ y] = self.world.chunks[chunk_position].blocks[x][y][z]
71
+
72
+ # save the chunk file
73
+
74
+ chunk_data["Level"]["Blocks"] = chunk_blocks
75
+ chunk_data.save(chunk_path, gzipped = True)
76
+
77
+ def load(self):
78
+ # for x in range(-16, 15):
79
+ # for y in range(-15, 16):
80
+ # self.load_chunk((x, 0, y))
81
+
82
+ # for x in range(-4, 4):
83
+ # for y in range(-4, 4):
84
+ # self.load_chunk((x, 0, y))
85
+
86
+ for x in range(-1, 1):
87
+ for y in range(-1, 1):
88
+ self.load_chunk((x, 0, y))
89
+
90
+ def save(self):
91
+ for chunk_position in self.world.chunks:
92
+ if chunk_position[1] != 0: # reject all chunks above and below the world limit
93
+ continue
94
+
95
+ chunk = self.world.chunks[chunk_position]
96
+
97
+ if chunk.modified:
98
+ self.save_chunk(chunk_position)
99
+ chunk.modified = False
shader.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import pyglet.gl as gl
3
+
4
+ class Shader_error(Exception):
5
+ def __init__(self, message):
6
+ self.message = message
7
+
8
+ def create_shader(target, source_path):
9
+ # read shader source
10
+
11
+ source_file = open(source_path, "rb")
12
+ source = source_file.read()
13
+ source_file.close()
14
+
15
+ source_length = ctypes.c_int(len(source) + 1)
16
+ source_buffer = ctypes.create_string_buffer(source)
17
+
18
+ buffer_pointer = ctypes.cast(
19
+ ctypes.pointer(ctypes.pointer(source_buffer)),
20
+ ctypes.POINTER(ctypes.POINTER(ctypes.c_char)))
21
+
22
+ # compile shader
23
+
24
+ gl.glShaderSource(target, 1, buffer_pointer, ctypes.byref(source_length))
25
+ gl.glCompileShader(target)
26
+
27
+ # handle potential errors
28
+
29
+ log_length = gl.GLint(0)
30
+ gl.glGetShaderiv(target, gl.GL_INFO_LOG_LENGTH, ctypes.byref(log_length))
31
+
32
+ log_buffer = ctypes.create_string_buffer(log_length.value)
33
+ gl.glGetShaderInfoLog(target, log_length, None, log_buffer)
34
+
35
+ if log_length.value > 1:
36
+ raise Shader_error(str(log_buffer.value))
37
+
38
+ class Shader:
39
+ def __init__(self, vert_path, frag_path):
40
+ self.program = gl.glCreateProgram()
41
+
42
+ # create vertex shader
43
+
44
+ self.vert_shader = gl.glCreateShader(gl.GL_VERTEX_SHADER)
45
+ create_shader(self.vert_shader, vert_path)
46
+ gl.glAttachShader(self.program, self.vert_shader)
47
+
48
+ # create fragment shader
49
+
50
+ self.frag_shader = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
51
+ create_shader(self.frag_shader, frag_path)
52
+ gl.glAttachShader(self.program, self.frag_shader)
53
+
54
+ # link program and clean up
55
+
56
+ gl.glLinkProgram(self.program)
57
+
58
+ gl.glDeleteShader(self.vert_shader)
59
+ gl.glDeleteShader(self.frag_shader)
60
+
61
+ def __del__(self):
62
+ gl.glDeleteProgram(self.program)
63
+
64
+ def find_uniform(self, name):
65
+ return gl.glGetUniformLocation(self.program, ctypes.create_string_buffer(name))
66
+
67
+ def uniform_matrix(self, location, matrix):
68
+ gl.glUniformMatrix4fv(location, 1, gl.GL_FALSE, (gl.GLfloat * 16) (*sum(matrix.data, [])))
69
+
70
+ def use(self):
71
+ gl.glUseProgram(self.program)
subchunk.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SUBCHUNK_WIDTH = 4
2
+ SUBCHUNK_HEIGHT = 4
3
+ SUBCHUNK_LENGTH = 4
4
+
5
+ class Subchunk:
6
+ def __init__(self, parent, subchunk_position):
7
+ self.parent = parent
8
+ self.world = self.parent.world
9
+
10
+ self.subchunk_position = subchunk_position
11
+
12
+ self.local_position = (
13
+ self.subchunk_position[0] * SUBCHUNK_WIDTH,
14
+ self.subchunk_position[1] * SUBCHUNK_HEIGHT,
15
+ self.subchunk_position[2] * SUBCHUNK_LENGTH)
16
+
17
+ self.position = (
18
+ self.parent.position[0] + self.local_position[0],
19
+ self.parent.position[1] + self.local_position[1],
20
+ self.parent.position[2] + self.local_position[2])
21
+
22
+ # mesh variables
23
+
24
+ self.mesh_vertex_positions = []
25
+ self.mesh_tex_coords = []
26
+ self.mesh_shading_values = []
27
+
28
+ self.mesh_index_counter = 0
29
+ self.mesh_indices = []
30
+
31
+ def update_mesh(self):
32
+ self.mesh_vertex_positions = []
33
+ self.mesh_tex_coords = []
34
+ self.mesh_shading_values = []
35
+
36
+ self.mesh_index_counter = 0
37
+ self.mesh_indices = []
38
+
39
+ def add_face(face):
40
+ vertex_positions = block_type.vertex_positions[face].copy()
41
+
42
+ for i in range(4):
43
+ vertex_positions[i * 3 + 0] += x
44
+ vertex_positions[i * 3 + 1] += y
45
+ vertex_positions[i * 3 + 2] += z
46
+
47
+ self.mesh_vertex_positions.extend(vertex_positions)
48
+
49
+ indices = [0, 1, 2, 0, 2, 3]
50
+ for i in range(6):
51
+ indices[i] += self.mesh_index_counter
52
+
53
+ self.mesh_indices.extend(indices)
54
+ self.mesh_index_counter += 4
55
+
56
+ self.mesh_tex_coords.extend(block_type.tex_coords[face])
57
+ self.mesh_shading_values.extend(block_type.shading_values[face])
58
+
59
+ for local_x in range(SUBCHUNK_WIDTH):
60
+ for local_y in range(SUBCHUNK_HEIGHT):
61
+ for local_z in range(SUBCHUNK_LENGTH):
62
+ parent_lx = self.local_position[0] + local_x
63
+ parent_ly = self.local_position[1] + local_y
64
+ parent_lz = self.local_position[2] + local_z
65
+
66
+ block_number = self.parent.blocks[parent_lx][parent_ly][parent_lz]
67
+
68
+ if block_number:
69
+ block_type = self.world.block_types[block_number]
70
+
71
+ x, y, z = (
72
+ self.position[0] + local_x,
73
+ self.position[1] + local_y,
74
+ self.position[2] + local_z)
75
+
76
+ def can_render_face(position):
77
+ if not self.world.is_opaque_block(position):
78
+ if block_type.glass and self.world.get_block_number(position) == block_number:
79
+ return False
80
+
81
+ return True
82
+
83
+ return False
84
+
85
+ # if block is cube, we want it to check neighbouring blocks so that we don't uselessly render faces
86
+ # if block isn't a cube, we just want to render all faces, regardless of neighbouring blocks
87
+ # since the vast majority of blocks are probably anyway going to be cubes, this won't impact performance all that much; the amount of useless faces drawn is going to be minimal
88
+
89
+ if block_type.is_cube:
90
+ if can_render_face((x + 1, y, z)): add_face(0)
91
+ if can_render_face((x - 1, y, z)): add_face(1)
92
+ if can_render_face((x, y + 1, z)): add_face(2)
93
+ if can_render_face((x, y - 1, z)): add_face(3)
94
+ if can_render_face((x, y, z + 1)): add_face(4)
95
+ if can_render_face((x, y, z - 1)): add_face(5)
96
+
97
+ else:
98
+ for i in range(len(block_type.vertex_positions)):
99
+ add_face(i)
texture_manager.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import pyglet
3
+
4
+ import pyglet.gl as gl
5
+
6
+ class Texture_manager:
7
+ def __init__(self, texture_width, texture_height, max_textures):
8
+ self.texture_width = texture_width
9
+ self.texture_height = texture_height
10
+
11
+ self.max_textures = max_textures
12
+
13
+ self.textures = []
14
+
15
+ self.texture_array = gl.GLuint(0)
16
+ gl.glGenTextures(1, self.texture_array)
17
+ gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.texture_array)
18
+
19
+ gl.glTexParameteri(gl.GL_TEXTURE_2D_ARRAY, gl.GL_TEXTURE_MIN_FILTER, gl.GL_NEAREST)
20
+ gl.glTexParameteri(gl.GL_TEXTURE_2D_ARRAY, gl.GL_TEXTURE_MAG_FILTER, gl.GL_NEAREST)
21
+
22
+ gl.glTexImage3D(
23
+ gl.GL_TEXTURE_2D_ARRAY, 0, gl.GL_RGBA,
24
+ self.texture_width, self.texture_height, self.max_textures,
25
+ 0, gl.GL_RGBA, gl.GL_UNSIGNED_BYTE, None)
26
+
27
+ def generate_mipmaps(self):
28
+ gl.glGenerateMipmap(gl.GL_TEXTURE_2D_ARRAY)
29
+
30
+ def add_texture(self, texture):
31
+ if not texture in self.textures:
32
+ self.textures.append(texture)
33
+
34
+ texture_image = pyglet.image.load(f"textures/{texture}.png").get_image_data()
35
+ gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.texture_array)
36
+
37
+ gl.glTexSubImage3D(
38
+ gl.GL_TEXTURE_2D_ARRAY, 0,
39
+ 0, 0, self.textures.index(texture),
40
+ self.texture_width, self.texture_height, 1,
41
+ gl.GL_RGBA, gl.GL_UNSIGNED_BYTE,
42
+ texture_image.get_data("RGBA", texture_image.width * 4))
vert.glsl ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #version 330
2
+
3
+ layout(location = 0) in vec3 vertex_position;
4
+ layout(location = 1) in vec3 tex_coords;
5
+ layout(location = 2) in float shading_value;
6
+
7
+ out vec3 local_position;
8
+ out vec3 interpolated_tex_coords;
9
+ out float interpolated_shading_value;
10
+
11
+ uniform mat4 matrix;
12
+
13
+ void main(void) {
14
+ local_position = vertex_position;
15
+ interpolated_tex_coords = tex_coords;
16
+ interpolated_shading_value = shading_value;
17
+ gl_Position = matrix * vec4(vertex_position, 1.0);
18
+ }
world.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import random
3
+
4
+ import save
5
+ import chunk
6
+
7
+ import block_type
8
+ import texture_manager
9
+
10
+ # import custom block models
11
+
12
+ import models
13
+
14
+ class World:
15
+ def __init__(self):
16
+ self.texture_manager = texture_manager.Texture_manager(16, 16, 256)
17
+ self.block_types = [None]
18
+
19
+ # parse block type data file
20
+
21
+ blocks_data_file = open("data/blocks.mcpy")
22
+ blocks_data = blocks_data_file.readlines()
23
+ blocks_data_file.close()
24
+
25
+ for block in blocks_data:
26
+ if block[0] in ['\n', '#']: # skip if empty line or comment
27
+ continue
28
+
29
+ number, props = block.split(':', 1)
30
+ number = int(number)
31
+
32
+ # default block
33
+
34
+ name = "Unknown"
35
+ model = models.cube
36
+ texture = {"all": "unknown"}
37
+
38
+ # read properties
39
+
40
+ for prop in props.split(','):
41
+ prop = prop.strip()
42
+ prop = list(filter(None, prop.split(' ', 1)))
43
+
44
+ if prop[0] == "sameas":
45
+ sameas_number = int(prop[1])
46
+
47
+ name = self.block_types[sameas_number].name
48
+ texture = self.block_types[sameas_number].block_face_textures
49
+ model = self.block_types[sameas_number].model
50
+
51
+ elif prop[0] == "name":
52
+ name = eval(prop[1])
53
+
54
+ elif prop[0][:7] == "texture":
55
+ _, side = prop[0].split('.')
56
+ texture[side] = prop[1].strip()
57
+
58
+ elif prop[0] == "model":
59
+ model = eval(prop[1])
60
+
61
+ # add block type
62
+
63
+ _block_type = block_type.Block_type(self.texture_manager, name, texture, model)
64
+
65
+ if number < len(self.block_types):
66
+ self.block_types[number] = _block_type
67
+
68
+ else:
69
+ self.block_types.append(_block_type)
70
+
71
+ self.texture_manager.generate_mipmaps()
72
+
73
+ # load the world
74
+
75
+ self.save = save.Save(self)
76
+
77
+ self.chunks = {}
78
+ self.save.load()
79
+
80
+ for chunk_position in self.chunks:
81
+ self.chunks[chunk_position].update_subchunk_meshes()
82
+ self.chunks[chunk_position].update_mesh()
83
+
84
+ def get_chunk_position(self, position):
85
+ x, y, z = position
86
+
87
+ return (
88
+ math.floor(x / chunk.CHUNK_WIDTH),
89
+ math.floor(y / chunk.CHUNK_HEIGHT),
90
+ math.floor(z / chunk.CHUNK_LENGTH))
91
+
92
+ def get_local_position(self, position):
93
+ x, y, z = position
94
+
95
+ return (
96
+ int(x % chunk.CHUNK_WIDTH),
97
+ int(y % chunk.CHUNK_HEIGHT),
98
+ int(z % chunk.CHUNK_LENGTH))
99
+
100
+ def get_block_number(self, position):
101
+ x, y, z = position
102
+ chunk_position = self.get_chunk_position(position)
103
+
104
+ if not chunk_position in self.chunks:
105
+ return 0
106
+
107
+ lx, ly, lz = self.get_local_position(position)
108
+
109
+ block_number = self.chunks[chunk_position].blocks[lx][ly][lz]
110
+ return block_number
111
+
112
+ def is_opaque_block(self, position):
113
+ # get block type and check if it's opaque or not
114
+ # air counts as a transparent block, so test for that too
115
+
116
+ block_type = self.block_types[self.get_block_number(position)]
117
+
118
+ if not block_type:
119
+ return False
120
+
121
+ return not block_type.transparent
122
+
123
+ def set_block(self, position, number): # set number to 0 (air) to remove block
124
+ x, y, z = position
125
+ chunk_position = self.get_chunk_position(position)
126
+
127
+ if not chunk_position in self.chunks: # if no chunks exist at this position, create a new one
128
+ if number == 0:
129
+ return # no point in creating a whole new chunk if we're not gonna be adding anything
130
+
131
+ self.chunks[chunk_position] = chunk.Chunk(self, chunk_position)
132
+
133
+ if self.get_block_number(position) == number: # no point updating mesh if the block is the same
134
+ return
135
+
136
+ lx, ly, lz = self.get_local_position(position)
137
+
138
+ self.chunks[chunk_position].blocks[lx][ly][lz] = number
139
+ self.chunks[chunk_position].modified = True
140
+
141
+ self.chunks[chunk_position].update_at_position((x, y, z))
142
+ self.chunks[chunk_position].update_mesh()
143
+
144
+ cx, cy, cz = chunk_position
145
+
146
+ def try_update_chunk_at_position(chunk_position, position):
147
+ if chunk_position in self.chunks:
148
+ self.chunks[chunk_position].update_at_position(position)
149
+ self.chunks[chunk_position].update_mesh()
150
+
151
+ if lx == chunk.CHUNK_WIDTH - 1: try_update_chunk_at_position((cx + 1, cy, cz), (x + 1, y, z))
152
+ if lx == 0: try_update_chunk_at_position((cx - 1, cy, cz), (x - 1, y, z))
153
+
154
+ if ly == chunk.CHUNK_HEIGHT - 1: try_update_chunk_at_position((cx, cy + 1, cz), (x, y + 1, z))
155
+ if ly == 0: try_update_chunk_at_position((cx, cy - 1, cz), (x, y - 1, z))
156
+
157
+ if lz == chunk.CHUNK_LENGTH - 1: try_update_chunk_at_position((cx, cy, cz + 1), (x, y, z + 1))
158
+ if lz == 0: try_update_chunk_at_position((cx, cy, cz - 1), (x, y, z - 1))
159
+
160
+ def try_set_block(self, pos, num, collider):
161
+ # if we're trying to remove a block, whatever let it go through
162
+
163
+ if not num:
164
+ return self.set_block(pos, 0)
165
+
166
+ # make sure the block doesn't intersect with the passed collider
167
+
168
+ for block_collider in self.block_types[num].colliders:
169
+ if collider & (block_collider + pos):
170
+ return
171
+
172
+ self.set_block(pos, num)
173
+
174
+ def draw(self):
175
+ for chunk_position in self.chunks:
176
+ self.chunks[chunk_position].draw()