Давайте модифицируем код, чтобы учесть все необходимые параметры для плавного перехода между режимами камеры. Мы добавим сохранение текущих значений position, yaw, pitch, distance, и height перед переключением режима камеры, а также будем использовать их для интерполяции во время перехода.
Вот код:
# --- Start of game_objects.py ---
import pyassimp  # Ensure this line is present
import numpy as np
from OpenGL.GL import *
from OpenGL.arrays import vbo

class FBXModel:
    def __init__(self, model_path="characterMedium.obj", position=(0, 0, -200), scale=1.0):
        self.position = list(position)
        self.scale = scale
        self.vbo_data = []

        # Load model using pyassimp
        try:
            with pyassimp.load(model_path) as scene:  # Use context manager
                if not scene or not scene.meshes:
                    raise ValueError("Failed to load model!")
                self.vbo_data = self.process_scene(scene)
        except Exception as e:
            print(f"Ошибка при загрузке модели: {e}")
            raise

        self.vbo = vbo.VBO(np.array(self.vbo_data, dtype=np.float32))

    def process_scene(self, scene):
        data = []
        for mesh in scene.meshes:
            for face in mesh.faces:
                for vertex_index in face:
                    vertex = mesh.vertices[vertex_index]
                    data.extend([vertex[0], vertex[1], vertex[2]])

                    if mesh.normals is not None and len(mesh.normals) > vertex_index:
                        normal = mesh.normals[vertex_index]
                        data.extend([normal[0], normal[1], normal[2]])
                    else:
                        data.extend([0, 0, 0])
        return data

    def render(self):
        glPushMatrix()
        glTranslatef(*self.position)
        glScalef(self.scale, self.scale, self.scale)

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

        stride = 6 * 4  # 6 floats per vertex (3 for position, 3 for normal), 4 bytes per float
        glVertexPointer(3, GL_FLOAT, stride, self.vbo)
        glNormalPointer(GL_FLOAT, stride, self.vbo + 12)

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

        glDisableClientState(GL_VERTEX_ARRAY)
        glDisableClientState(GL_NORMAL_ARRAY)
        self.vbo.unbind()

        glPopMatrix()

    # Add this method to update the position
    def update_position(self, new_position):
        """Update the position of the model."""
        self.position = list(new_position)  # Ensure position is updated as a list
# --- 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 FBXModel
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

# Флаг для режима камеры от третьего лица
third_person_mode = False

plane = Plane(size=20000, grid=50, heightmap="Height_Map.exr", diffusemap="Diffuse_Map.exr", height_scale=20000)
model = FBXModel(model_path="characterMedium.obj", position=[player_x, player_y + 50, player_z], scale=15)
interface = GameInterface(minimap_filename="mini_map.png", minimap_size=200)

noclip_mode = False
third_person_camera_height = 10
first_person_offset_forward = 1  # Смещение вперёд (в метрах)
first_person_offset_up = 2       # Смещение вверх (в метрах)

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()

    if third_person_mode:
        # Режим камеры от третьего лица
        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))

        # Позиция камеры от третьего лица
        camera_offset_x = player_x - math.sin(math.radians(yaw)) * distance
        camera_offset_z = player_z + math.cos(math.radians(yaw)) * distance
        camera_offset_y = player_y + third_person_camera_height

        gluLookAt(
            camera_offset_x, camera_offset_y, camera_offset_z,
            look_x, look_y, look_z,  # Цель камеры смотрит на игрока
            0, 1, 0  # Вектор "вверх" остаётся неизменным
        )
    else:
        # Режим камеры от первого лица
        # Расчёт смещения камеры
        camera_offset_x = player_x + math.sin(math.radians(yaw)) * first_person_offset_forward
        camera_offset_z = player_z - math.cos(math.radians(yaw)) * first_person_offset_forward
        camera_offset_y = player_y + first_person_offset_up

        look_x = camera_offset_x + math.sin(math.radians(yaw)) * math.cos(math.radians(pitch))
        look_y = camera_offset_y + math.sin(math.radians(pitch))
        look_z = camera_offset_z - math.cos(math.radians(yaw)) * math.cos(math.radians(pitch))

        gluLookAt(
            camera_offset_x, camera_offset_y, camera_offset_z,
            look_x, look_y, 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(*model.position, camera_yaw, camera_pitch, camera_distance)

    glPushMatrix()
    plane.render()
    model.render()  # Рендерим модель вместо куба
    glPopMatrix()

    glDisable(GL_DEPTH_TEST)
    interface.render_minimap(model.position[0], model.position[2], 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
        elif event.type == pygame.KEYDOWN and event.key == pygame.K_f:  # Переключение режима камеры
            third_person_mode = not third_person_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_model_position = model.position.copy()

    if noclip_mode:
        new_x = model.position[0] + forward_x * move_forward + strafe_x * move_strafe
        new_z = model.position[2] + forward_z * move_forward + strafe_z * move_strafe
        vertical_momentum = 0
        new_y = model.position[1] + (keys[pygame.K_SPACE] - keys[pygame.K_LCTRL]) * current_move_speed * dt
        model.update_position([new_x, new_y, new_z])
    else:
        new_x = model.position[0] + forward_x * move_forward + strafe_x * move_strafe
        new_z = model.position[2] + forward_z * move_forward + strafe_z * move_strafe

        terrain_height = plane.get_height(new_x, new_z)
        if model.position[1] < terrain_height:
            model.position[1] = terrain_height
            vertical_momentum = 0
            is_grounded = True

        if not is_grounded:
            vertical_momentum -= gravity * dt
            model.position[1] += vertical_momentum
        else:
            vertical_momentum = 0

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

        model.update_position([new_x, model.position[1], new_z])

    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 ---