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



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