Переведи код игры на Ursina:
# --- Start of game_objects.py ---
import math
from OpenGL.GL import *
from OpenGL.arrays import vbo
import numpy as np

class Cube:
    def __init__(self, size=50, position=(0, 0, -200)):
        self.size = size
        self.position = list(position)
        self.vertices = [
            ((-size / 2, -size / 2, -size / 2), (0, 0, -1)),
            ((size / 2, -size / 2, -size / 2), (0, 0, -1)),
            ((size / 2, size / 2, -size / 2), (0, 0, -1)),
            ((-size / 2, size / 2, -size / 2), (0, 0, -1)),
            ((-size / 2, -size / 2, size / 2), (0, 0, 1)),
            ((size / 2, -size / 2, size / 2), (0, 0, 1)),
            ((size / 2, size / 2, size / 2), (0, 0, 1)),
            ((-size / 2, size / 2, size / 2), (0, 0, 1)),
        ]
        self.faces = [
            (0, 1, 2, 3),
            (4, 5, 6, 7),
            (0, 1, 5, 4),
            (2, 3, 7, 6),
            (1, 2, 6, 5),
            (0, 3, 7, 4),
        ]
        self.vbo_data = self.generate_vbo_data()
        self.vbo = vbo.VBO(np.array(self.vbo_data, dtype=np.float32))

    def generate_vbo_data(self):
        data = []
        for face in self.faces:
            for vertex_index in face:
                normal = self.vertices[vertex_index][1]
                vertex = self.vertices[vertex_index][0]
                data.extend(normal)
                data.extend(vertex)
        return data

    def render(self):
        glPushMatrix()
        glTranslatef(*self.position)
        self.vbo.bind()
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_NORMAL_ARRAY)
        glVertexPointer(3, GL_FLOAT, 24, self.vbo + 12)
        glNormalPointer(GL_FLOAT, 24, self.vbo)
        glDrawArrays(GL_QUADS, 0, len(self.vbo_data) // 6)
        glDisableClientState(GL_VERTEX_ARRAY)
        glDisableClientState(GL_NORMAL_ARRAY)
        self.vbo.unbind()
        glPopMatrix()
# --- End of game_objects.py ---

# --- Start of interface_module.py ---
from PIL import Image
import numpy as np
from OpenGL.GL import *
from OpenGL.GLU import *

class GameInterface:
    def __init__(self, minimap_filename="mini_map.png", minimap_size=200):
        self.minimap_texture = self.load_minimap_texture(minimap_filename)
        self.minimap_size = minimap_size

    def load_minimap_texture(self, filename):
        """Загружает текстуру миникарты из файла PNG."""
        img = Image.open(filename).transpose(Image.FLIP_TOP_BOTTOM)
        if img.mode != "RGB":
            img = img.convert("RGB")
        img_data = np.array(list(img.getdata()), np.uint8)

        texture_id = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, texture_id)

        glEnable(GL_TEXTURE_2D)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.size[0], img.size[1], 0, GL_RGB, GL_UNSIGNED_BYTE, img_data)
        return texture_id

    def render_minimap(self, player_x, player_z, plane_size):
        glPushMatrix()

        glDisable(GL_DEPTH_TEST)

        glMatrixMode(GL_PROJECTION)
        glPushMatrix()
        glLoadIdentity()
        gluOrtho2D(0, 800, 0, 600)

        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()

        glEnable(GL_TEXTURE_2D)
        glBindTexture(GL_TEXTURE_2D, self.minimap_texture)
        glBegin(GL_QUADS)
        glTexCoord2f(0, 0)
        glVertex2f(800 - self.minimap_size, 600 - self.minimap_size)
        glTexCoord2f(1, 0)
        glVertex2f(800, 600 - self.minimap_size)
        glTexCoord2f(1, 1)
        glVertex2f(800, 600)
        glTexCoord2f(0, 1)
        glVertex2f(800 - self.minimap_size, 600)
        glEnd()
        glDisable(GL_TEXTURE_2D)

        glColor3f(1, 0, 0)
        glBegin(GL_QUADS)
        vertex_size = 2
        x = (player_x / plane_size) * self.minimap_size + (800 - self.minimap_size/2)
        z = (player_z / plane_size) * self.minimap_size + (600 - self.minimap_size/2)
        glVertex2f(x - vertex_size, z - vertex_size)
        glVertex2f(x + vertex_size, z - vertex_size)
        glVertex2f(x + vertex_size, z + vertex_size)
        glVertex2f(x - vertex_size, z + vertex_size)
        glEnd()

        glMatrixMode(GL_PROJECTION)
        glPopMatrix()
        glMatrixMode(GL_MODELVIEW)

        glEnable(GL_DEPTH_TEST)
        glPopMatrix()
# --- End of interface_module.py ---

# --- Start of main.py ---
import pygame
import math
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
from plane_module import Plane
from game_objects import Cube
from interface_module import GameInterface

pygame.init()
screen = pygame.display.set_mode((800, 600), DOUBLEBUF | OPENGL)
pygame.display.set_caption("3D Shooter with OpenGL")

glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)

glLightfv(GL_LIGHT0, GL_POSITION, (0, 100, 0, 1))
glLightfv(GL_LIGHT0, GL_AMBIENT, (0.2, 0.2, 0.2, 1))
glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.8, 0.8, 0.8, 1))

player_x, player_y, player_z = 0, 0, 0
move_speed = 0.8  # Скорость движения в обычном режиме
jump_force = 1.5
gravity = 0.08

camera_distance = 20
camera_height = 15
camera_pitch = 0
camera_yaw = 0
mouse_sensitivity = 0.2
min_pitch, max_pitch = -89, 89

# Указываем height_scale для более выраженных перепадов высот
plane = Plane(size=20000, grid=50, heightmap="Height_Map.exr", diffusemap="Diffuse_Map.exr", height_scale=20000)
cube = Cube(size=10, position=[0, 5, -200])
interface = GameInterface(minimap_filename="mini_map.png", minimap_size=200)

# Флаг для режима noclip
noclip_mode = False

def set_camera(player_x, player_y, player_z, yaw, pitch, distance):
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(60, (800 / 600), 0.1, 10000.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()

    look_x = player_x + math.sin(math.radians(yaw)) * math.cos(math.radians(pitch))
    look_y = player_y + math.sin(math.radians(pitch))
    look_z = player_z - math.cos(math.radians(yaw)) * math.cos(math.radians(pitch))

    gluLookAt(
        player_x, player_y + camera_height, player_z,
        look_x, look_y + camera_height, look_z,
        0, 1, 0
    )

clock = pygame.time.Clock()
pygame.mouse.set_visible(False)
pygame.event.set_grab(True)
vertical_momentum = 0
is_grounded = True
running = True

while running:
    dt = clock.tick(60) * 0.1

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glClearColor(0.53, 0.81, 0.98, 1)

    set_camera(player_x, player_y, player_z, camera_yaw, camera_pitch, camera_distance)

    glPushMatrix()
    plane.render()
    cube.render()
    glPopMatrix()

    glDisable(GL_DEPTH_TEST)
    interface.render_minimap(player_x, player_z, plane_size=20000)
    glEnable(GL_DEPTH_TEST)

    for event in pygame.event.get():
        if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE):
            running = False
        elif event.type == pygame.KEYDOWN and event.key == pygame.K_n:  # Переключение режима noclip
            noclip_mode = not noclip_mode

    dx, dy = pygame.mouse.get_rel()
    camera_yaw += dx * mouse_sensitivity
    camera_pitch -= dy * mouse_sensitivity
    camera_pitch = max(min_pitch, min(max_pitch, camera_pitch))

    keys = pygame.key.get_pressed()
    
    # Определяем текущую скорость в зависимости от режима
    current_move_speed = move_speed * 10 if noclip_mode else move_speed
    
    move_forward = (keys[pygame.K_w] - keys[pygame.K_s]) * current_move_speed * dt
    move_strafe = (keys[pygame.K_d] - keys[pygame.K_a]) * current_move_speed * dt

    angle = math.radians(camera_yaw)
    forward_x = math.sin(angle)
    forward_z = -math.cos(angle)
    strafe_x = math.cos(angle)
    strafe_z = math.sin(angle)

    old_player_x, old_player_z = player_x, player_z

    if noclip_mode:
        # В режиме noclip игнорируем высоту местности и гравитацию
        player_x += forward_x * move_forward + strafe_x * move_strafe
        player_z += forward_z * move_forward + strafe_z * move_strafe
        vertical_momentum = 0
        player_y += (keys[pygame.K_SPACE] - keys[pygame.K_LCTRL]) * current_move_speed * dt
    else:
        # В обычном режиме сохраняем логику столкновений и гравитации
        player_x += forward_x * move_forward + strafe_x * move_strafe
        player_z += forward_z * move_forward + strafe_z * move_strafe

        terrain_height = plane.get_height(player_x, player_z)
        if player_y < terrain_height:
            player_y = terrain_height
            vertical_momentum = 0
            is_grounded = True

        if not is_grounded:
            vertical_momentum -= gravity * dt
            player_y += vertical_momentum
        else:
            vertical_momentum = 0

        if is_grounded and keys[pygame.K_SPACE]:
            vertical_momentum = jump_force
            is_grounded = False

    pygame.display.flip()

pygame.quit()
# --- End of main.py ---

# --- Start of plane_module.py ---
import OpenEXR
import Imath
import numpy as np
from PIL import Image
import math
from OpenGL.GL import *
from OpenGL.arrays import vbo

class Plane:
    def __init__(self, size=20000, grid=50, heightmap="Height_Map.exr", diffusemap="Diffuse_Map.exr", height_scale=4000):
        self.size = size  # Увеличен базовый размер в 20 раз (1000 -> 20000)
        self.grid = grid
        self.height_scale = height_scale  # Новый параметр для масштабирования высот
        self.heightmap = self.load_heightmap(heightmap)
        self.diffuse_texture = self.load_diffuse_texture(diffusemap)
        self.points = self.generate_points()
        self.triangles = self.generate_triangles()
        self.vbo_data = self.generate_vbo_data()
        self.vbo = vbo.VBO(np.array(self.vbo_data, dtype=np.float32))

    def load_heightmap(self, filename):
        """Загружает карту высот из файла EXR."""
        exr_file = OpenEXR.InputFile(filename)
        dw = exr_file.header()['dataWindow']
        width, height = dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1
        red_channel = np.frombuffer(exr_file.channel('R', Imath.PixelType(Imath.PixelType.FLOAT)), dtype=np.float32)
        red_channel.shape = (height, width)
        return red_channel

    def load_diffuse_texture(self, filename):
        """Загружает текстуру из файла EXR."""
        exr_file = OpenEXR.InputFile(filename)
        dw = exr_file.header()['dataWindow']
        size = (dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1)

        # Получаем каналы R, G, B
        channels = exr_file.channels(['R', 'G', 'B'], Imath.PixelType(Imath.PixelType.FLOAT))
        red_channel = np.frombuffer(channels[0], dtype=np.float32).reshape(size[1], size[0])
        green_channel = np.frombuffer(channels[1], dtype=np.float32).reshape(size[1], size[0])
        blue_channel = np.frombuffer(channels[2], dtype=np.float32).reshape(size[1], size[0])

        # Собираем данные в RGB-текстуру
        img_data = np.stack([red_channel, green_channel, blue_channel], axis=-1)

        # Создаем текстуру в OpenGL
        texture_id = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, texture_id)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, size[0], size[1], 0, GL_RGB, GL_FLOAT, img_data)
        glGenerateMipmap(GL_TEXTURE_2D)

        # Устанавливаем параметры текстуры
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

        return texture_id

    def get_height(self, x, z):
        width, height = self.heightmap.shape
        # Масштабируем координаты согласно новому размеру
        x_index = int((x + self.size / 2) / self.size * (width - 1))
        z_index = int((z + self.size / 2) / self.size * (height - 1))
        if 0 <= x_index < width and 0 <= z_index < height:
            return self.heightmap[z_index, x_index] * self.height_scale  # Используем height_scale
        else:
            return 0

    def generate_points(self):
        points = []
        for x in range(-self.size, self.size + 1, self.grid):
            for z in range(-self.size, self.size + 1, self.grid):
                y = self.get_height(x, z)
                points.append((x, y, z))
        return points

    def generate_triangles(self):
        triangles = []
        width = self.size * 2 // self.grid + 1
        for i in range(len(self.points) - width - 1):
            if (i + 1) % width == 0:
                continue
            triangles.append([self.points[i], self.points[i + 1], self.points[i + width]])
            triangles.append([self.points[i + 1], self.points[i + width + 1], self.points[i + width]])
        return triangles

    def calculate_normal(self, triangle):
        v1 = (
            triangle[1][0] - triangle[0][0],
            triangle[1][1] - triangle[0][1],
            triangle[1][2] - triangle[0][2],
        )
        v2 = (
            triangle[2][0] - triangle[0][0],
            triangle[2][1] - triangle[0][1],
            triangle[2][2] - triangle[0][2],
        )
        normal = (
            v1[1] * v2[2] - v1[2] * v2[1],
            v1[2] * v2[0] - v1[0] * v2[2],
            v1[0] * v2[1] - v1[1] * v2[0],
        )
        length = math.sqrt(normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2)
        return (
            normal[0] / length,
            normal[1] / length,
            normal[2] / length,
        )

    def generate_vbo_data(self):
        data = []
        for triangle in self.triangles:
            normal = self.calculate_normal(triangle)
            for vertex in triangle:
                data.extend(normal)
                data.extend(vertex)
                # Текстурные координаты
                tex_u = (vertex[0] + self.size) / (2 * self.size)
                tex_v = (vertex[2] + self.size) / (2 * self.size)
                data.extend([tex_u, tex_v])
        return data

    def render(self):
        glEnable(GL_TEXTURE_2D)
        glBindTexture(GL_TEXTURE_2D, self.diffuse_texture)
        glColor3f(1, 1, 1)  # Уберите цвет, чтобы текстура не смешивалась с материалом

        self.vbo.bind()
        glEnableClientState(GL_VERTEX_ARRAY)
        glEnableClientState(GL_NORMAL_ARRAY)
        glEnableClientState(GL_TEXTURE_COORD_ARRAY)

        stride = 32  # 12 (нормаль) + 12 (вершина) + 8 (текстура)
        glNormalPointer(GL_FLOAT, stride, self.vbo)
        glVertexPointer(3, GL_FLOAT, stride, self.vbo + 12)
        glTexCoordPointer(2, GL_FLOAT, stride, self.vbo + 24)

        glDrawArrays(GL_TRIANGLES, 0, len(self.vbo_data) // 8)

        glDisableClientState(GL_VERTEX_ARRAY)
        glDisableClientState(GL_NORMAL_ARRAY)
        glDisableClientState(GL_TEXTURE_COORD_ARRAY)
        self.vbo.unbind()
        glDisable(GL_TEXTURE_2D)
# --- End of plane_module.py ---