The rotations here seem to interfere with one another as you continue to rotate along the pitch and roll axes. Can this be corrected? please output the entire code.



import glfw
from OpenGL.GL import *
from OpenGL.GLU import *
import numpy as np
import sys
import math
from PIL import Image
import random

# Function to load materials from the .mtl file
def load_mtl(filename):
    materials = {}
    current_mtl = None

    try:
        with open(filename, 'r') as f:
            for line in f:
                if line.startswith('newmtl'):
                    parts = line.strip().split(None, 1)
                    if len(parts) > 1:
                        current_mtl = parts[1]
                        materials[current_mtl] = {
                            'Kd': [1.0, 1.0, 1.0],  # Diffuse color default to white
                            'map_Kd': None          # Texture map
                        }
                elif line.startswith('Kd') and current_mtl:
                    parts = line.strip().split()
                    materials[current_mtl]['Kd'] = list(map(float, parts[1:4]))
                elif line.startswith('map_Kd') and current_mtl:
                    parts = line.strip().split(None, 1)
                    if len(parts) > 1:
                        materials[current_mtl]['map_Kd'] = parts[1]
    except IOError:
        print(f"Error: Unable to open {filename}")
        sys.exit(1)

    return materials

# OBJ loader with materials and textures
def load_obj(filename):
    vertices = []
    tex_coords = []
    normals = []
    faces = []
    materials = {}
    current_mtl = None
    mtl_files = []

    try:
        with open(filename, 'r') as f:
            for line in f:
                if line.startswith('mtllib'):
                    parts = line.strip().split(None, 1)
                    if len(parts) > 1:
                        mtl_files.append(parts[1])
                elif line.startswith('v '):
                    parts = line.strip().split()
                    vertex = list(map(float, parts[1:4]))
                    vertices.append(vertex)
                elif line.startswith('vt '):
                    parts = line.strip().split()
                    tex_coord = list(map(float, parts[1:3]))
                    tex_coords.append(tex_coord)
                elif line.startswith('vn '):
                    parts = line.strip().split()
                    normal = list(map(float, parts[1:4]))
                    normals.append(normal)
                elif line.startswith('usemtl'):
                    parts = line.strip().split(None, 1)
                    if len(parts) > 1:
                        current_mtl = parts[1]
                elif line.startswith('f '):
                    parts = line.strip().split()
                    face = []
                    for part in parts[1:]:
                        indices = part.split('/')
                        v_idx = int(indices[0]) - 1
                        vt_idx = int(indices[1]) - 1 if len(indices) > 1 and indices[1] else None
                        vn_idx = int(indices[2]) - 1 if len(indices) > 2 and indices[2] else None
                        face.append({'v': v_idx, 'vt': vt_idx, 'vn': vn_idx})
                    face.append(current_mtl)  # Append material to face
                    faces.append(face)
            if not vertices or not faces:
                print(f"Error: {filename} contains no vertices or faces.")
                sys.exit(1)
            print(f"Loaded {len(vertices)} vertices and {len(faces)} faces from {filename}")
    except IOError:
        print(f"Error: Unable to open {filename}")
        sys.exit(1)

    # Load materials from the material files
    for mtl_file in mtl_files:
        mtl_data = load_mtl(mtl_file)
        materials.update(mtl_data)

    return {
        'vertices': np.array(vertices, dtype='float32'),
        'tex_coords': np.array(tex_coords, dtype='float32'),
        'normals': np.array(normals, dtype='float32'),
        'faces': faces,
        'materials': materials
    }

# Function to load textures from materials
def load_textures(materials):
    texture_ids = {}
    for mtl_name, mtl_props in materials.items():
        texture_file = mtl_props.get('map_Kd')
        if texture_file:
            texture_id = load_texture(texture_file)
            texture_ids[mtl_name] = texture_id
        else:
            texture_ids[mtl_name] = None
    return texture_ids

# Function to load a texture image
def load_texture(filename):
    try:
        texture_image = Image.open(filename)
        texture_image = texture_image.convert('RGB')
        texture_data = texture_image.tobytes("raw", "RGB", 0, -1)
        width, height = texture_image.size

        texture_id = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, texture_id)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, texture_data)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

        return texture_id
    except IOError:
        print(f"Error: Unable to load texture {filename}")
        sys.exit(1)

# Function to create a rotation matrix given an axis and angle
def rotation_matrix(axis, angle):
    """
    Create a rotation matrix given an axis and angle.
    """
    axis = axis / np.linalg.norm(axis)
    cos_a = np.cos(angle)
    sin_a = np.sin(angle)
    one_minus_cos = 1.0 - cos_a
    x, y, z = axis
    r = np.array([
        [cos_a + x * x * one_minus_cos,
         x * y * one_minus_cos - z * sin_a,
         x * z * one_minus_cos + y * sin_a],
        [y * x * one_minus_cos + z * sin_a,
         cos_a + y * y * one_minus_cos,
         y * z * one_minus_cos - x * sin_a],
        [z * x * one_minus_cos - y * sin_a,
         z * y * one_minus_cos + x * sin_a,
         cos_a + z * z * one_minus_cos]
    ], dtype='float32')
    return r

# Camera class for handling position and orientation
class Camera:
    def __init__(self, distance=15.0, height=5.0):
        self.distance = distance
        self.height = height
        self.position = np.array([0.0, 0.0, 0.0])
        self.target = np.array([0.0, 0.0, 0.0])
        self.up = np.array([0.0, 1.0, 0.0])

    def update(self, ship_position, ship_rotation_matrix):
        # Camera offset in local space (behind the ship)
        camera_offset = np.array([0.0, self.height, -self.distance])
        # Transform to world space
        cam_offset_world = ship_rotation_matrix @ camera_offset
        self.position = ship_position + cam_offset_world
        self.target = ship_position
        # Up vector transformed by ship's rotation
        self.up = ship_rotation_matrix @ np.array([0.0, 1.0, 0.0])

    def apply_view(self):
        gluLookAt(
            self.position[0], self.position[1], self.position[2],
            self.target[0], self.target[1], self.target[2],
            self.up[0], self.up[1], self.up[2]
        )

# Function to draw a sphere without using GLUT
def draw_sphere(radius, slices, stacks):
    quadric = gluNewQuadric()
    gluSphere(quadric, radius, slices, stacks)
    gluDeleteQuadric(quadric)

# Projectile class for handling bullets
class Projectile:
    def __init__(self, position, direction, speed=1.0):
        self.position = np.array(position, dtype='float32')
        self.direction = np.array(direction, dtype='float32')
        self.speed = speed
        self.alive = True  # Used for removing projectile when out of range or time

    def update(self, delta_time):
        self.position += self.direction * self.speed * delta_time

    def draw(self):
        glPushMatrix()
        glTranslatef(self.position[0], self.position[1], self.position[2])
        glColor3f(1.0, 1.0, 0.0)  # Yellow color for projectiles
        draw_sphere(0.2, 8, 8)  # Use the new draw_sphere function
        glPopMatrix()

# Initialize GLFW and create a window
if not glfw.init():
    print("Failed to initialize GLFW")
    sys.exit()

window = glfw.create_window(800, 600, "3D Ship Viewer", None, None)
if not window:
    print("Failed to create GLFW window")
    glfw.terminate()
    sys.exit()

glfw.make_context_current(window)

# Set up viewport and projection
def window_resize(window, width, height):
    if height == 0:
        height = 1
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45, width / height, 0.1, 1000.0)
    glMatrixMode(GL_MODELVIEW)

glfw.set_window_size_callback(window, window_resize)
# Initial setup
window_size = glfw.get_window_size(window)
window_resize(window, window_size[0], window_size[1])

# Enable depth testing and lighting
glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
# Set light properties
glLightfv(GL_LIGHT0, GL_POSITION, [0, 100, 100, 1])
glLightfv(GL_LIGHT0, GL_AMBIENT, [0.2, 0.2, 0.2, 1.0])
glLightfv(GL_LIGHT0, GL_DIFFUSE, [0.8, 0.8, 0.8, 1.0])
glLightfv(GL_LIGHT0, GL_SPECULAR, [1.0, 1.0, 1.0, 1.0])
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
glShadeModel(GL_SMOOTH)

# Enable texturing
glEnable(GL_TEXTURE_2D)

# Load ship model
ship_data = load_obj('Low Poly Spaceship.obj')
ship_vertices = ship_data['vertices']
tex_coords = ship_data['tex_coords']
normals = ship_data['normals']
ship_faces = ship_data['faces']
materials = ship_data['materials']

# Load textures from materials
texture_ids = load_textures(materials)

# Generate stars within a smaller range for visibility
num_stars = 1000
star_positions = np.random.uniform(-50, 50, (num_stars, 3))

# Initialize ship position and orientation
ship_pos = np.array([0.0, 0.0, 0.0], dtype='float32')
ship_vel = np.array([0.0, 0.0, 0.0], dtype='float32')  # Ship's velocity

# Initialize ship rotation matrix as identity
ship_rotation_matrix = np.identity(3, dtype='float32')

# Initialize camera
camera = Camera(distance=15.0, height=5.0)

# Movement variables
acceleration = 5.0  # Increased acceleration rate for better responsiveness
rotate_speed = 1.5  # Degrees per frame
friction = 0.5      # Friction coefficient for slowing down
max_speed = 20.0    # Maximum speed

# Turbulence variables
turbulence_intensity = 0.2  # Adjust this value for more or less turbulence

# Key state
keys = {}

def key_callback(window, key, scancode, action, mods):
    global keys
    if action == glfw.PRESS:
        keys[key] = True
    elif action == glfw.RELEASE:
        keys[key] = False

glfw.set_key_callback(window, key_callback)

# Initialize projectiles list
projectiles = []

# Timing
last_time = glfw.get_time()

# Main loop
while not glfw.window_should_close(window):
    # Calculate delta time
    current_time = glfw.get_time()
    delta_time = current_time - last_time
    last_time = current_time

    # Handle input
    # Rotation
    if glfw.KEY_UP in keys and keys[glfw.KEY_UP]:
        # Pitch up
        angle = np.radians(rotate_speed)
        rot_axis = ship_rotation_matrix @ np.array([1.0, 0.0, 0.0])  # Ship's local X-axis
        rotation = rotation_matrix(axis=rot_axis, angle=angle)
        ship_rotation_matrix = ship_rotation_matrix @ rotation
    if glfw.KEY_DOWN in keys and keys[glfw.KEY_DOWN]:
        # Pitch down
        angle = -np.radians(rotate_speed)
        rot_axis = ship_rotation_matrix @ np.array([1.0, 0.0, 0.0])  # Ship's local X-axis
        rotation = rotation_matrix(axis=rot_axis, angle=angle)
        ship_rotation_matrix = ship_rotation_matrix @ rotation
    if glfw.KEY_LEFT in keys and keys[glfw.KEY_LEFT]:
        # Roll left
        angle = -np.radians(rotate_speed)
        rot_axis = ship_rotation_matrix @ np.array([0.0, 0.0, 1.0])  # Ship's local Z-axis
        rotation = rotation_matrix(axis=rot_axis, angle=angle)
        ship_rotation_matrix = ship_rotation_matrix @ rotation
    if glfw.KEY_RIGHT in keys and keys[glfw.KEY_RIGHT]:
        # Roll right
        angle = np.radians(rotate_speed)
        rot_axis = ship_rotation_matrix @ np.array([0.0, 0.0, 1.0])  # Ship's local Z-axis
        rotation = rotation_matrix(axis=rot_axis, angle=angle)
        ship_rotation_matrix = ship_rotation_matrix @ rotation
    if glfw.KEY_Q in keys and keys[glfw.KEY_Q]:
        # Yaw left
        angle = np.radians(rotate_speed)
        rot_axis = ship_rotation_matrix @ np.array([0.0, 1.0, 0.0])  # Ship's local Y-axis
        rotation = rotation_matrix(axis=rot_axis, angle=angle)
        ship_rotation_matrix = ship_rotation_matrix @ rotation
    if glfw.KEY_E in keys and keys[glfw.KEY_E]:
        # Yaw right
        angle = -np.radians(rotate_speed)
        rot_axis = ship_rotation_matrix @ np.array([0.0, 1.0, 0.0])  # Ship's local Y-axis
        rotation = rotation_matrix(axis=rot_axis, angle=angle)
        ship_rotation_matrix = ship_rotation_matrix @ rotation

    # Movement
    # Compute ship's forward, right, and up directions
    forward_direction = ship_rotation_matrix @ np.array([0.0, 0.0, 1.0])
    right_direction = ship_rotation_matrix @ np.array([1.0, 0.0, 0.0])

    # Apply acceleration
    if glfw.KEY_W in keys and keys[glfw.KEY_W]:
        ship_vel += acceleration * forward_direction * delta_time
    if glfw.KEY_S in keys and keys[glfw.KEY_S]:
        ship_vel -= acceleration * forward_direction * delta_time
    if glfw.KEY_A in keys and keys[glfw.KEY_A]:
        # Strafe left
        ship_vel -= acceleration * right_direction * delta_time
    if glfw.KEY_D in keys and keys[glfw.KEY_D]:
        # Strafe right
        ship_vel += acceleration * right_direction * delta_time

    # Apply turbulence
    turbulence = turbulence_intensity * (np.random.rand(3) - 0.5) * delta_time
    ship_vel += turbulence

    # Limit speed
    speed = np.linalg.norm(ship_vel)
    if speed > max_speed:
        ship_vel = (ship_vel / speed) * max_speed

    # Apply friction
    ship_vel -= ship_vel * friction * delta_time

    # Update ship position
    ship_pos += ship_vel * delta_time

    # Shooting
    if glfw.KEY_SPACE in keys and keys[glfw.KEY_SPACE]:
        # Fire projectile
        # Reset the key state to prevent continuous firing
        keys[glfw.KEY_SPACE] = False

        # Create a new projectile
        projectile_speed = 50.0
        projectile = Projectile(
            position=ship_pos.copy(),
            direction=forward_direction.copy(),
            speed=projectile_speed
        )
        projectiles.append(projectile)

    # Update projectiles
    for projectile in projectiles[:]:
        projectile.update(delta_time)
        # Remove projectile if it's too far away
        if np.linalg.norm(projectile.position - ship_pos) > 500:
            projectiles.remove(projectile)

    # Update camera based on ship's new position and orientation
    camera.update(ship_position=ship_pos, ship_rotation_matrix=ship_rotation_matrix)

    # Clear buffers
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glLoadIdentity()

    # Apply camera view
    camera.apply_view()

    # Draw stars
    glDisable(GL_LIGHTING)  # Disable lighting for stars
    glPointSize(2.0)
    glColor3f(1.0, 1.0, 1.0)
    glBegin(GL_POINTS)
    for star in star_positions:
        glVertex3f(star[0], star[1], star[2])
    glEnd()
    glEnable(GL_LIGHTING)  # Re-enable lighting

    # Draw projectiles
    glDisable(GL_TEXTURE_2D)  # Disable texturing for projectiles
    for projectile in projectiles:
        projectile.draw()
    glEnable(GL_TEXTURE_2D)

    # Convert 3x3 rotation matrix to 4x4
    ship_rotation_matrix_4x4 = np.identity(4, dtype='float32')
    ship_rotation_matrix_4x4[:3, :3] = ship_rotation_matrix

    # Draw ship
    glPushMatrix()
    glTranslatef(ship_pos[0], ship_pos[1], ship_pos[2])

    # Apply ship rotations using the 4x4 matrix
    glMultMatrixf(ship_rotation_matrix_4x4.T)

    current_mtl = None

    glBegin(GL_TRIANGLES)
    for face in ship_faces:
        mtl_name = face[-1]  # Last element is the material name
        if mtl_name != current_mtl:
            glEnd()
            current_mtl = mtl_name
            mtl_props = materials.get(mtl_name, {})
            texture_id = texture_ids.get(mtl_name)
            if texture_id:
                glBindTexture(GL_TEXTURE_2D, texture_id)
                glEnable(GL_TEXTURE_2D)
                glColor3f(1.0, 1.0, 1.0)
            else:
                glDisable(GL_TEXTURE_2D)
                Kd = mtl_props.get('Kd', [1.0, 1.0, 1.0])
                glColor3fv(Kd)
            glBegin(GL_TRIANGLES)
        for vertex_info in face[:-1]:  # Exclude the material name
            v_idx = vertex_info['v']
            vt_idx = vertex_info['vt']
            vn_idx = vertex_info['vn']
            if vn_idx is not None and len(normals) > vn_idx:
                glNormal3fv(normals[vn_idx])
            else:
                # If normals are not provided, compute or use a default
                glNormal3f(0.0, 0.0, 1.0)
            if vt_idx is not None and len(tex_coords) > vt_idx:
                glTexCoord2fv(tex_coords[vt_idx])
            glVertex3fv(ship_vertices[v_idx])
    glEnd()
    glPopMatrix()

    # Swap buffers and poll events
    glfw.swap_buffers(window)
    glfw.poll_events()

# Terminate GLFW
glfw.terminate()