diff --git a/.gitignore b/.gitignore index 5d381cc..4fac0e8 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +/__pycache__/ +/mesh/__pycache__/ +/world_objects/__pycache__/ diff --git a/assets/frame.png b/assets/frame.png new file mode 100644 index 0000000..9bab97a Binary files /dev/null and b/assets/frame.png differ diff --git a/assets/test.png b/assets/test.png new file mode 100644 index 0000000..d4e7855 Binary files /dev/null and b/assets/test.png differ diff --git a/camera.py b/camera.py new file mode 100644 index 0000000..0070c5a --- /dev/null +++ b/camera.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +import glm + +from settings import * + + +class Camera: + def __init__(self, position, yaw, pitch): + self.position = glm.vec3(position) + self.yaw = glm.radians(yaw) + self.pitch = glm.radians(pitch) + + self.up = glm.vec3(0, 1, 0) + self.right = glm.vec3(1, 0, 0) + self.forward = glm.vec3(0, 0, -1) + + self.m_proj = glm.perspective(V_FOV, ASPECT_RATIO, NEAR, FAR) + self.m_view = glm.mat4() + + def update(self): + self.update_vectors() + self.update_view_matrix() + + def update_view_matrix(self): + self.m_view = glm.lookAt(self.position, self.position + self.forward, self.up) + + def update_vectors(self): + self.forward.x = glm.cos(self.yaw) * glm.cos(self.pitch) + self.forward.y = glm.sin(self.pitch) + self.forward.z = glm.sin(self.yaw) * glm.cos(self.pitch) + + self.forward = glm.normalize(self.forward) + self.right = glm.normalize(glm.cross(self.forward, glm.vec3(0, 1, 0))) + self.up = glm.normalize(glm.cross(self.right, self.forward)) + + def rotate_pitch(self, delta_y): + self.pitch -= delta_y + self.pitch = glm.clamp(self.pitch, -PITCH_MAX, PITCH_MAX) + + def rotate_yaw(self, delta_x): + self.yaw += delta_x + + def move_left(self, velocity): + self.position -= self.right * velocity + + def move_right(self, velocity): + self.position += self.right * velocity + + def move_up(self, velocity): + self.position += self.up * velocity + + def move_down(self, velocity): + self.position -= self.up * velocity + + def move_forward(self, velocity): + self.position += self.forward * velocity + + def move_back(self, velocity): + self.position -= self.forward * velocity diff --git a/engine.py b/engine.py new file mode 100644 index 0000000..27b66ec --- /dev/null +++ b/engine.py @@ -0,0 +1,75 @@ +#!/usr/bin/python3 + +from settings import * +import moderngl as mgl +import pygame as pg +import sys +from shader_program import ShaderProgram +from scene import Scene +from player import Player +from textures import Textures + + +class VoxelEngine: + def __init__(self): + pg.init() + pg.display.gl_set_attribute(pg.GL_CONTEXT_MAJOR_VERSION, 3) + pg.display.gl_set_attribute(pg.GL_CONTEXT_MINOR_VERSION, 3) + pg.display.gl_set_attribute(pg.GL_CONTEXT_PROFILE_MASK, pg.GL_CONTEXT_PROFILE_CORE) + pg.display.gl_set_attribute(pg.GL_DEPTH_SIZE, 24) + + pg.display.set_mode(WIN_RES, flags=pg.OPENGL | pg.DOUBLEBUF) + self.ctx = mgl.create_context() + + self.ctx.enable(flags=mgl.DEPTH_TEST | mgl.CULL_FACE | mgl.BLEND) + self.ctx.gc_mode = "auto" + + self.clock = pg.time.Clock() + self.delta_time = 0 + self.time = 0 + + pg.event.set_grab(True) + pg.mouse.set_visible(False) + + self.on_init() + + self.running = True + + def on_init(self): + self.textures = Textures(self) + self.player = Player(self) + self.shader_program = ShaderProgram(self) + self.scene = Scene(self) + + def update(self): + self.player.update() + self.shader_program.update() + self.scene.update() + + self.delta_time = self.clock.tick() + self.time = pg.time.get_ticks() * 0.001 + pg.display.set_caption(f"{self.clock.get_fps() :.0f}") + + def render(self): + self.ctx.clear(color=BG_COLOR) + self.scene.render() + pg.display.flip() + + def handle_events(self): + for event in pg.event.get(): + if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE): + self.running = False + + def run(self): + while self.running: + self.handle_events() + self.update() + self.render() + + pg.quit() + sys.exit() + + +if __name__ == "__main__": + app = VoxelEngine() + app.run() \ No newline at end of file diff --git a/mesh/base.py b/mesh/base.py new file mode 100644 index 0000000..d51d8ac --- /dev/null +++ b/mesh/base.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +import numpy + + +class BaseMesh: + def __init__(self): + # OpenGL context + self.ctx = None + # shader program + self.program = None + # vertex buffer data type format: "3f 3f" + self.vbo_format = None + # attribute names according to the format: ("in_position", "in_color") + self.attributes: tuple[str, ...] = None + # vertex array object + self.vao = None + + def get_vertex_data(self) -> numpy.array: ... + + def get_vao(self): + vertex_data = self.get_vertex_data() + vbo = self.ctx.buffer(vertex_data) + vao = self.ctx.vertex_array( + self.program, + [(vbo, self.vbo_format, *self.attributes)], + skip_errors=True + ) + return vao + + def render(self): + self.vao.render() \ No newline at end of file diff --git a/mesh/chunk.py b/mesh/chunk.py new file mode 100644 index 0000000..344298a --- /dev/null +++ b/mesh/chunk.py @@ -0,0 +1,28 @@ +#!/usr/bin/python3 + +from mesh.base import BaseMesh +from mesh.chunk_builder import build_chunk_mesh + + +class ChunkMesh(BaseMesh): + def __init__(self, chunk): + super().__init__() + self.app = chunk.app + self.chunk = chunk + self.ctx = self.app.ctx + self.program = self.app.shader_program.chunk + + self.vbo_format = "3u1 1u1 1u1 1u1 1u1" + self.format_size = sum(int(fmt[:1]) for fmt in self.vbo_format.split()) + self.attributes = ("in_position", "voxel_id", "face_id", "ao_id", "flip_id") + self.vao = self.get_vao() + + def get_vertex_data(self): + mesh = build_chunk_mesh( + chunk_voxels=self.chunk.voxels, + format_size=self.format_size, + chunk_pos=self.chunk.position, + world_voxels=self.chunk.world.voxels + ) + + return mesh \ No newline at end of file diff --git a/mesh/chunk_builder.py b/mesh/chunk_builder.py new file mode 100644 index 0000000..d36f582 --- /dev/null +++ b/mesh/chunk_builder.py @@ -0,0 +1,211 @@ +#!/usr/bin/python3 + +from settings import * +from numba import uint8 + + +@njit +def get_ao(local_pos, world_pos, world_voxels, plane): + x, y, z = local_pos + wx, wy, wz = world_pos + + if plane == "Y": + a = is_void((x , y, z - 1), (wx , wy, wz - 1), world_voxels) + b = is_void((x - 1, y, z - 1), (wx - 1, wy, wz - 1), world_voxels) + c = is_void((x - 1, y, z ), (wx - 1, wy, wz ), world_voxels) + d = is_void((x - 1, y, z + 1), (wx - 1, wy, wz + 1), world_voxels) + e = is_void((x , y, z + 1), (wx , wy, wz + 1), world_voxels) + f = is_void((x + 1, y, z + 1), (wx + 1, wy, wz + 1), world_voxels) + g = is_void((x + 1, y, z ), (wx + 1, wy, wz ), world_voxels) + h = is_void((x + 1, y, z - 1), (wx + 1, wy, wz - 1), world_voxels) + + elif plane == "X": + a = is_void((x, y , z - 1), (wx, wy , wz - 1), world_voxels) + b = is_void((x, y - 1, z - 1), (wx, wy - 1, wz - 1), world_voxels) + c = is_void((x, y - 1, z ), (wx, wy - 1, wz ), world_voxels) + d = is_void((x, y - 1, z + 1), (wx, wy - 1, wz + 1), world_voxels) + e = is_void((x, y , z + 1), (wx, wy , wz + 1), world_voxels) + f = is_void((x, y + 1, z + 1), (wx, wy + 1, wz + 1), world_voxels) + g = is_void((x, y + 1, z ), (wx, wy + 1, wz ), world_voxels) + h = is_void((x, y + 1, z - 1), (wx, wy + 1, wz - 1), world_voxels) + + else: # Z plane + a = is_void((x - 1, y , z), (wx - 1, wy , z), world_voxels) + b = is_void((x - 1, y - 1, z), (wx - 1, wy - 1, z), world_voxels) + c = is_void((x , y - 1, z), (wx , wy - 1, z), world_voxels) + d = is_void((x + 1, y - 1, z), (wx + 1, wy - 1, z), world_voxels) + e = is_void((x + 1, y , z), (wx + 1, wy , z), world_voxels) + f = is_void((x + 1, y + 1, z), (wx + 1, wy + 1, z), world_voxels) + g = is_void((x , y + 1, z), (wx , wy + 1, z), world_voxels) + h = is_void((x - 1, y + 1, z), (wx - 1, wy + 1, z), world_voxels) + + ao = (a + b + c), (g + h + a), (e + f + g), (c + d + e) + return ao + + +@njit +def to_uint8(x, y, z, voxel_id, face_id, ao_id, flip_id): + return (uint8(x), uint8(y), uint8(z), + uint8(voxel_id), uint8(face_id), uint8(ao_id), uint8(flip_id) + ) + + +@njit +def get_chunk_index(world_voxel_pos): + wx, wy, wz = world_voxel_pos + cx = wx // CHUNK_SIZE + cy = wy // CHUNK_SIZE + cz = wz // CHUNK_SIZE + + if not (0 <= cx < WORLD_W and 0 <= cy < WORLD_H and 0 <= cz < WORLD_D): + return -1 + + index = cx + WORLD_W * cz + WORLD_AREA * cy + return index + + +@njit +def is_void(local_voxel_pos, world_voxel_pos, world_voxels): + chunk_index = get_chunk_index(world_voxel_pos) + + if chunk_index == -1: + return False + + chunk_voxels = world_voxels[chunk_index] + x, y, z = local_voxel_pos + voxel_index = x % CHUNK_SIZE + z % CHUNK_SIZE * CHUNK_SIZE + y % CHUNK_SIZE * CHUNK_AREA + + if chunk_voxels[voxel_index]: + return False + + return True + + +@njit +def add_data(vertex_data, index, *vertices): + for vertex in vertices: + for attribute in vertex: + vertex_data[index] = attribute + index += 1 + + return index + + +@njit +def build_chunk_mesh(chunk_voxels, format_size, chunk_pos, world_voxels): + vertex_data = numpy.empty(CHUNK_VOL * 18 * format_size, dtype="uint8") + index = 0 + + for x in range(CHUNK_SIZE): + for y in range(CHUNK_SIZE): + for z in range(CHUNK_SIZE): + voxel_id = chunk_voxels[x + CHUNK_SIZE * z + CHUNK_AREA * y] + + if not voxel_id: + continue + + # voxel world position + cx, cy, cz = chunk_pos + wx = x + cx * CHUNK_SIZE + wy = y + cy * CHUNK_SIZE + wz = z + cz * CHUNK_SIZE + + # top face + if is_void((x, y + 1, z), (wx, wy + 1, wz), world_voxels): + # get ao values + ao = get_ao((x, y + 1, z), (wx, wy + 1, wz), world_voxels, plane="Y") + flip_id = ao[1] + ao[3] > ao[0] + ao[2] + + # format: x, y, z, voxel_id, face_id, ao_id + v0 = to_uint8(x , y + 1, z , voxel_id, 0, ao[0], flip_id) + v1 = to_uint8(x + 1, y + 1, z , voxel_id, 0, ao[1], flip_id) + v2 = to_uint8(x + 1, y + 1, z + 1, voxel_id, 0, ao[2], flip_id) + v3 = to_uint8(x , y + 1, z + 1, voxel_id, 0, ao[3], flip_id) + + if flip_id: + index = add_data(vertex_data, index, v1, v0, v3, v1, v3, v2) + + else: + index = add_data(vertex_data, index, v0, v3, v2, v0, v2, v1) + + # bottom face + if is_void((x, y - 1, z), (wx, wy - 1, wz), world_voxels): + ao = get_ao((x, y - 1, z), (wx, wy - 1, wz), world_voxels, plane="Y") + flip_id = ao[1] + ao[3] > ao[0] + ao[2] + + v0 = to_uint8(x , y, z , voxel_id, 1, ao[0], flip_id) + v1 = to_uint8(x + 1, y, z , voxel_id, 1, ao[1], flip_id) + v2 = to_uint8(x + 1, y, z + 1, voxel_id, 1, ao[2], flip_id) + v3 = to_uint8(x , y, z + 1, voxel_id, 1, ao[3], flip_id) + + if flip_id: + index = add_data(vertex_data, index, v1, v3, v0, v1, v2, v3) + + else: + index = add_data(vertex_data, index, v0, v2, v3, v0, v1, v2) + + # right face + if is_void((x + 1, y, z), (wx + 1, wy, wz), world_voxels): + ao = get_ao((x + 1, y, z), (wx + 1, wy, wz), world_voxels, plane="X") + flip_id = ao[1] + ao[3] > ao[0] + ao[2] + + v0 = to_uint8(x + 1, y , z , voxel_id, 2, ao[0], flip_id) + v1 = to_uint8(x + 1, y + 1, z , voxel_id, 2, ao[1], flip_id) + v2 = to_uint8(x + 1, y + 1, z + 1, voxel_id, 2, ao[2], flip_id) + v3 = to_uint8(x + 1, y , z + 1, voxel_id, 2, ao[3], flip_id) + + if flip_id: + index = add_data(vertex_data, index, v3, v0, v1, v3, v1, v2) + + else: + index = add_data(vertex_data, index, v0, v1, v2, v0, v2, v3) + + # left face + if is_void((x - 1, y, z), (wx - 1, wy, wz), world_voxels): + ao = get_ao((x - 1, y, z), (wx - 1, wy, wz), world_voxels, plane="X") + flip_id = ao[1] + ao[3] > ao[0] + ao[2] + + v0 = to_uint8(x, y , z , voxel_id, 3, ao[0], flip_id) + v1 = to_uint8(x, y + 1, z , voxel_id, 3, ao[1], flip_id) + v2 = to_uint8(x, y + 1, z + 1, voxel_id, 3, ao[2], flip_id) + v3 = to_uint8(x, y , z + 1, voxel_id, 3, ao[3], flip_id) + + if flip_id: + index = add_data(vertex_data, index, v3, v1, v0, v3, v2, v1) + + else: + index = add_data(vertex_data, index, v0, v2, v1, v0, v3, v2) + + # back face + if is_void((x, y, z - 1), (wx, wy, wz - 1), world_voxels): + ao = get_ao((x, y, z - 1), (wx, wy, wz - 1), world_voxels, plane="Z") + flip_id = ao[1] + ao[3] > ao[0] + ao[2] + + v0 = to_uint8(x , y , z, voxel_id, 4, ao[0], flip_id) + v1 = to_uint8(x , y + 1, z, voxel_id, 4, ao[1], flip_id) + v2 = to_uint8(x + 1, y + 1, z, voxel_id, 4, ao[2], flip_id) + v3 = to_uint8(x + 1, y , z, voxel_id, 4, ao[3], flip_id) + + if flip_id: + index = add_data(vertex_data, index, v3, v0, v1, v3, v1, v2) + + else: + index = add_data(vertex_data, index, v0, v1, v2, v0, v2, v3) + + # front face + if is_void((x, y, z + 1), (wx, wy, wz + 1), world_voxels): + ao = get_ao((x, y, z + 1), (wx, wy, wz + 1), world_voxels, plane="Z") + flip_id = ao[1] + ao[3] > ao[0] + ao[2] + + v0 = to_uint8(x , y , z + 1, voxel_id, 5, ao[0], flip_id) + v1 = to_uint8(x , y + 1, z + 1, voxel_id, 5, ao[1], flip_id) + v2 = to_uint8(x + 1, y + 1, z + 1, voxel_id, 5, ao[2], flip_id) + v3 = to_uint8(x + 1, y , z + 1, voxel_id, 5, ao[3], flip_id) + + if flip_id: + index = add_data(vertex_data, index, v3, v1, v0, v3, v2, v1) + + else: + index = add_data(vertex_data, index, v0, v2, v1, v0, v3, v2) + + return vertex_data[:index + 1] \ No newline at end of file diff --git a/mesh/quad.py b/mesh/quad.py new file mode 100644 index 0000000..d4ffdf1 --- /dev/null +++ b/mesh/quad.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +from settings import * +from mesh.base import BaseMesh + + +class QuadMesh(BaseMesh): + def __init__(self, app): + super().__init__() + + self.app = app + self.ctx = app.ctx + self.program = app.shader_program.quad + + self.vbo_format = "3f 3f" + self.attributes = ("in_position", "in_color") + self.vao = self.get_vao() + + def get_vertex_data(self): + vertices = [ + (0.5, 0.5, 0.0), (-0.5, 0.5, 0.0), (-0.5, -0.5, 0.0), + (0.5, 0.5, 0.0), (-0.5, -0.5, 0.0), (0.5, -0.5, 0.0) + ] + + colors = [ + (0, 1, 0), (1, 0, 0), (1, 1, 0), + (0, 1, 0), (1, 1, 0), (0, 0, 1) + ] + + vertex_data = numpy.hstack([vertices, colors], dtype="float32") + return vertex_data \ No newline at end of file diff --git a/player.py b/player.py new file mode 100644 index 0000000..24f0b6c --- /dev/null +++ b/player.py @@ -0,0 +1,47 @@ +#!/usr/bin/python3 + +import pygame as pg +from camera import Camera +from settings import * + + +class Player(Camera): + def __init__(self, app, position=PLAYER_POS, yaw=-90, pitch=0): + self.app = app + super().__init__(position, yaw, pitch) + + def update(self): + self.keyboard_control() + self.mouse_control() + super().update() + + def mouse_control(self): + mouse_dx, mouse_dy = pg.mouse.get_rel() + + if mouse_dx: + self.rotate_yaw(delta_x=mouse_dx * MOUSE_SENSITIVITY) + + if mouse_dy: + self.rotate_pitch(delta_y=mouse_dy * MOUSE_SENSITIVITY) + + def keyboard_control(self): + key_state = pg.key.get_pressed() + vel = PLAYER_SPEED * self.app.delta_time + + if key_state[pg.K_w]: + self.move_forward(vel) + + if key_state[pg.K_s]: + self.move_back(vel) + + if key_state[pg.K_d]: + self.move_right(vel) + + if key_state[pg.K_a]: + self.move_left(vel) + + if key_state[pg.K_q]: + self.move_up(vel) + + if key_state[pg.K_e]: + self.move_down(vel) \ No newline at end of file diff --git a/scene.py b/scene.py new file mode 100644 index 0000000..49a5eb6 --- /dev/null +++ b/scene.py @@ -0,0 +1,16 @@ +#!/usr/bin/python3 + +from settings import * +from world import World + + +class Scene: + def __init__(self, app): + self.app = app + self.world = World(self.app) + + def update(self): + self.world.update() + + def render(self): + self.world.render() \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..81f9d82 --- /dev/null +++ b/settings.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 + +from numba import njit +import numpy +import glm +import math + +# resolution +WIN_RES = glm.vec2(1600, 1000) + +# chunk + +CHUNK_SIZE = 32 +H_CHUNK_SIZE = CHUNK_SIZE // 2 +CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE +CHUNK_VOL = CHUNK_AREA * CHUNK_SIZE + +# world +WORLD_W, WORLD_H = 20, 3 +WORLD_D = WORLD_W +WORLD_AREA = WORLD_W * WORLD_D +WORLD_VOL = WORLD_AREA * WORLD_H + +# world center + +CENTER_XZ = WORLD_W * H_CHUNK_SIZE +CENTER_Y = WORLD_H * H_CHUNK_SIZE + +# camera +ASPECT_RATIO = WIN_RES.x / WIN_RES.y +FOV_DEG = 50 +V_FOV = glm.radians(FOV_DEG) # vertical fov +H_FOV = 2 * math.atan(math.tan(V_FOV * 0.5) * ASPECT_RATIO) +NEAR = 0.1 +FAR = 2000.0 +PITCH_MAX = glm.radians(89) + +# player +PLAYER_SPEED = 0.055 +PLAYER_ROT_SPEED = 0.003 +PLAYER_POS = glm.vec3(CENTER_XZ, WORLD_H * CHUNK_SIZE, CENTER_XZ) +MOUSE_SENSITIVITY = 0.002 + +# colors + +BG_COLOR = glm.vec3(0.1, 0.16, 0.25) \ No newline at end of file diff --git a/shader_program.py b/shader_program.py new file mode 100644 index 0000000..7ecf8b2 --- /dev/null +++ b/shader_program.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +from settings import * + + +class ShaderProgram: + def __init__(self, app): + self.app = app + self.ctx = app.ctx + self.player = app.player + # -------- shaders -------- # + self.chunk = self.get_program(shader_name="chunk") + # ------------------------- # + self.set_uniforms_on_init() + self.chunk["u_texture_0"] = 0 + + def set_uniforms_on_init(self): + self.chunk["m_proj"].write(self.player.m_proj) + self.chunk["m_model"].write(glm.mat4()) + + def update(self): + self.chunk["m_view"].write(self.player.m_view) + + def get_program(self, shader_name): + with open(f"shaders/{shader_name}.vert") as file: + vertex_shader = file.read() + + with open(f"shaders/{shader_name}.frag") as file: + fragment_shader = file.read() + + program = self.ctx.program(vertex_shader=vertex_shader, fragment_shader=fragment_shader) + return program \ No newline at end of file diff --git a/shaders/chunk.frag b/shaders/chunk.frag new file mode 100644 index 0000000..e9073c1 --- /dev/null +++ b/shaders/chunk.frag @@ -0,0 +1,25 @@ +#version 330 core + +layout (location = 0) out vec4 fragColor; + +const vec3 gamma = vec3(2.2); +const vec3 inv_gamma = 1 / gamma; + +uniform sampler2D u_texture_0; + +in vec3 voxel_color; +in vec2 uv; +in float shading; + + +void main() { + vec3 tex_col = texture(u_texture_0, uv).rgb; + tex_col = pow(tex_col, gamma); + + tex_col.rgb *= voxel_color; + //tex_col = tex_col * 0.001 + vec3(1); + tex_col *= shading; + + tex_col = pow(tex_col, inv_gamma); + fragColor = vec4(tex_col, 1); +} diff --git a/shaders/chunk.vert b/shaders/chunk.vert new file mode 100644 index 0000000..985238c --- /dev/null +++ b/shaders/chunk.vert @@ -0,0 +1,51 @@ +#version 330 core + +layout (location = 0) in ivec3 in_position; +layout (location = 1) in int voxel_id; +layout (location = 2) in int face_id; +layout (location = 3) in int ao_id; +layout (location = 4) in int flip_id; + +uniform mat4 m_proj; +uniform mat4 m_view; +uniform mat4 m_model; + +out vec3 voxel_color; +out vec2 uv; +out float shading; + +const float ao_values[4] = float[4](0.1, 0.25, 0.1, 1.0); + +const float face_shading[6] = float[6]( + 1.0, 0.5, // top bottom + 0.5, 0.8, // right left + 0.5, 0.8 // front back +); + +const vec2 uv_coords[4] = vec2[4]( + vec2(0, 0), vec2(0, 1), + vec2(1, 0), vec2(1, 1) +); + +const int uv_indices[24] = int[24]( + 1, 0, 2, 1, 2, 3, // tex coord indices for vertices of an even face + 3, 0, 2, 3, 1, 0, // odd face + 3, 1, 0, 3, 0, 2, // even flipped face + 1, 2, 3, 1, 0, 2 // odd flipped face +); + + +vec3 hash31(float p) { + vec3 p3 = fract(vec3(p * 21.2) * vec3(0.1031, 0.1030, 0.0973)); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.xxy + p3.yzz) * p3.zyx) + 0.05; +} + + +void main() { + int uv_index = gl_VertexID % 6 + ((face_id & 1) + flip_id * 2) * 6; + uv = uv_coords[uv_indices[uv_index]]; + voxel_color = hash31(voxel_id); + shading = face_shading[face_id] * ao_values[ao_id]; + gl_Position = m_proj * m_view * m_model * vec4(in_position, 1.0); +} diff --git a/shaders/quad.frag b/shaders/quad.frag new file mode 100644 index 0000000..21e2df3 --- /dev/null +++ b/shaders/quad.frag @@ -0,0 +1,10 @@ +#version 330 core + +layout (location = 0) out vec4 fragColor; + +in vec3 color; + + +void main() { + fragColor = vec4(color, 1.0); +} \ No newline at end of file diff --git a/shaders/quad.vert b/shaders/quad.vert new file mode 100644 index 0000000..660a7dc --- /dev/null +++ b/shaders/quad.vert @@ -0,0 +1,16 @@ +#version 330 core + +layout (location = 0) in vec3 in_position; +layout (location = 1) in vec3 in_color; + +uniform mat4 m_proj; +uniform mat4 m_view; +uniform mat4 m_model; + +out vec3 color; + + +void main() { + color = in_color; + gl_Position = m_proj * m_view * m_model * vec4(in_position, 1.0); +} \ No newline at end of file diff --git a/textures.py b/textures.py new file mode 100644 index 0000000..161f814 --- /dev/null +++ b/textures.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 + +import pygame as pg +import moderngl as mgl + + +class Textures: + def __init__(self, app): + self.app = app + self.ctx = app.ctx + + # load textures + #self.texture_0 = self.load("frame.png") + self.texture_0 = self.load("test.png") + + # assign texture unit + self.texture_0.use(location=0) + + def load(self, file_name): + texture = pg.image.load(f"assets/{file_name}") + texture = pg.transform.flip(texture, flip_x=True, flip_y=False) + + texture = self.ctx.texture( + size=texture.get_size(), + components=4, + data=pg.image.tostring(texture, "RGBA", False) + ) + + texture.anisotropy = 32.0 + texture.build_mipmaps() + texture.filter = (mgl.NEAREST, mgl.NEAREST) + return texture \ No newline at end of file diff --git a/world.py b/world.py new file mode 100644 index 0000000..c66252d --- /dev/null +++ b/world.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 + +from settings import * +from world_objects.chunk import Chunk + + +class World: + def __init__(self, app): + self.app = app + self.chunks = [None for _ in range(WORLD_VOL)] + self.voxels = numpy.empty([WORLD_VOL, CHUNK_VOL], dtype="uint8") + self.build_chunks() + self.build_chunk_mesh() + + def build_chunks(self): + for x in range(WORLD_W): + for y in range(WORLD_H): + for z in range(WORLD_D): + chunk = Chunk(self, position=(x, y, z)) + + chunk_index = x + WORLD_W * z + WORLD_AREA * y + self.chunks[chunk_index] = chunk + + # put the chunk voxels in a separate array + self.voxels[chunk_index] = chunk.build_voxels() + + # get pointer to voxels + chunk.voxels = self.voxels[chunk_index] + + def build_chunk_mesh(self): + for chunk in self.chunks: + chunk.build_mesh() + + def update(self): + pass + + def render(self): + for chunk in self.chunks: + chunk.render() \ No newline at end of file diff --git a/world_objects/chunk.py b/world_objects/chunk.py new file mode 100644 index 0000000..18d3389 --- /dev/null +++ b/world_objects/chunk.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 + +from settings import * +from mesh.chunk import ChunkMesh + + +class Chunk: + def __init__(self, world, position): + self.app = world.app + self.world = world + self.position = position + self.m_model = self.get_model_matrix() + self.voxels: numpy.array = None + self.mesh: ChunkMesh = None + self.is_empty = True + + def get_model_matrix(self): + m_model = glm.translate(glm.mat4(), glm.vec3(self.position) * CHUNK_SIZE) + return m_model + + def set_uniform(self): + self.mesh.program["m_model"].write(self.m_model) + + def build_mesh(self): + self.mesh = ChunkMesh(self) + + def render(self): + self.set_uniform() + self.mesh.render() + + def build_voxels(self): + # empty chunk + voxels = numpy.zeros(CHUNK_VOL, dtype="uint8") + + # fill chunk + cx, cy, cz = glm.ivec3(self.position) * CHUNK_SIZE + + # fill chunk + for x in range(CHUNK_SIZE): + for z in range(CHUNK_SIZE): + wx = x + cx + wz = z + cz + world_height = int(glm.simplex(glm.vec2(wx, wz) * 0.01) * 32 + 32) + local_height = min(world_height - cy, CHUNK_SIZE) + + for y in range(local_height): + wy = y + cy + + voxels[x + CHUNK_SIZE * z + CHUNK_AREA * y] = wy + 1 + + if numpy.any(voxels): + self.is_empty = False + + return voxels \ No newline at end of file