--global-fix

This commit is contained in:
2026-02-04 21:31:57 +01:00
commit e5591e7dc8
24 changed files with 1848 additions and 0 deletions

BIN
my3dengine/SDL2.dll Normal file

Binary file not shown.

63
my3dengine/UI.py Normal file
View File

@@ -0,0 +1,63 @@
import my3dengine as m3d
from OpenGL.GL import *
import numpy as np
from .shader import create_ui_shader
import ctypes
class UI:
def __init__(self, window_width, window_height):
self.w = window_width
self.h = window_height
self.shader = create_ui_shader()
self.vao = glGenVertexArrays(1)
self.vbo = glGenBuffers(1)
glBindVertexArray(self.vao)
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
# Bufor na max 6 wierzchołków * 2 float (x,y)
glBufferData(GL_ARRAY_BUFFER, 6 * 2 * 4, None, GL_DYNAMIC_DRAW)
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * 4, ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
self.rects = [] # lista prostokątów do narysowania
def rect(self, x, y, width, height, color):
# dodaj prostokąt do listy, nie rysuj od razu
self.rects.append((x, y, width, height, color))
def draw(self):
glUseProgram(self.shader)
glBindVertexArray(self.vao)
loc_color = glGetUniformLocation(self.shader, "uColor")
loc_win = glGetUniformLocation(self.shader, "uWindowSize")
glUniform2f(loc_win, self.w, self.h)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
for x, y, w, h, color in self.rects:
vertices = np.array([
x, y,
x + w, y,
x, y + h,
x + w, y,
x + w, y + h,
x, y + h,
], dtype=np.float32)
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.nbytes, vertices)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glUniform4fv(loc_color, 1, color)
glDrawArrays(GL_TRIANGLES, 0, 6)
glBindVertexArray(0)
glUseProgram(0)
self.rects.clear() # po narysowaniu czyścimy listę

15
my3dengine/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
import os
import sys
# Automatyczna ścieżka do SDL2.dll
dll_path = os.path.abspath(os.path.dirname(__file__))
os.environ["PYSDL2_DLL_PATH"] = dll_path
from .key import Key
from .core import Window, Vector3, get_fullscreen, init, normalize, debug_init
from .camera import Camera
from .mesh import Mesh
from .UI import UI
from .audio import AudioSource, AudioListener, update_audio_system
__all__ = ['Window', 'Camera', 'Mesh', 'Key', 'UI', 'audio']

174
my3dengine/audio.py Normal file
View File

@@ -0,0 +1,174 @@
import numpy as np
import sounddevice as sd
import soundfile as sf
import threading
class AudioListener:
def __init__(self, position):
self.position = np.array(position, dtype=float)
class AudioSource:
def __init__(self, position, sound_path, emit_radius=15.0, volume=1.0, loop=False, fade_duration=1.0):
self.position = np.array(position, dtype=float)
self.sound_path = sound_path
self.emit_radius = emit_radius
self.base_volume = volume
self.loop = loop
self._data, self._fs = sf.read(self.sound_path, dtype='float32')
self._stream = None
self._stop_flag = threading.Event()
self._lock = threading.Lock()
self._volume = 0.0 # aktualna głośność (płynna)
self._target_volume = 0.0 # docelowa głośność (fade target)
self._fade_duration = fade_duration # czas fade w sekundach
self._pos = 0
self.playing = False
self._paused = False
self.MIN_VOLUME_TO_PLAY = 0.01
def _callback(self, outdata, frames, time, status):
with self._lock:
if self._stop_flag.is_set():
raise sd.CallbackStop()
chunk_start = self._pos
chunk_end = self._pos + frames
if chunk_end > len(self._data):
if self.loop:
part1 = self._data[chunk_start:]
part2 = self._data[:chunk_end - len(self._data)]
chunk = np.concatenate((part1, part2))
self._pos = chunk_end - len(self._data)
else:
chunk = self._data[chunk_start:]
chunk = np.pad(chunk, ((0, frames - len(chunk)), (0, 0)), mode='constant')
self._pos = len(self._data)
else:
chunk = self._data[chunk_start:chunk_end]
self._pos = chunk_end
current_vol = self._volume
target_vol = self._target_volume
fade_step = 1.0 / (self._fs * self._fade_duration) # ile głośności na 1 próbkę
steps = np.arange(frames, dtype=np.float32)
if target_vol > current_vol:
volumes = current_vol + fade_step * steps * (target_vol - current_vol)
volumes = np.clip(volumes, 0.0, target_vol)
else:
volumes = current_vol - fade_step * steps * (current_vol - target_vol)
volumes = np.clip(volumes, target_vol, 1.0)
self._volume = volumes[-1]
if self._paused or self._volume < self.MIN_VOLUME_TO_PLAY:
outdata[:] = 0
else:
if chunk.ndim > 1:
volumes = volumes[:, np.newaxis]
outdata[:] = chunk * volumes
if not self.loop and self._pos >= len(self._data):
raise sd.CallbackStop()
def _on_finished(self):
with self._lock:
self._stream = None
self.playing = False
self._volume = 0.0
self._target_volume = 0.0
self._pos = 0
self._paused = False
def play(self, volume):
with self._lock:
if self.playing:
if self._paused:
# Jeśli jest pauza, wznow i ustaw docelową głośność
self._paused = False
self._target_volume = volume
return
else:
# Już gra, więc ustaw tylko docelową głośność
self._target_volume = volume
return
# Nowe odtwarzanie od zera z głośnością 0 i fade-in
self._volume = 0.0
self._target_volume = volume
self._pos = 0
self._stop_flag.clear()
self._stream = sd.OutputStream(
samplerate=self._fs,
channels=self._data.shape[1] if self._data.ndim > 1 else 1,
callback=self._callback,
finished_callback=self._on_finished
)
self._stream.start()
self.playing = True
self._paused = False
def pause(self):
with self._lock:
if self.playing and not self._paused:
self._target_volume = 0.0 # fade out do 0, ale stream działa
# _paused ustawiane w callbacku, gdy głośność spadnie do zera
def resume(self):
with self._lock:
if self.playing and self._paused:
self._paused = False
self._volume = 0.0 # zaczynamy z 0, fade-in w callbacku
self._target_volume = self.base_volume # docelowa głośność
def stop(self):
with self._lock:
if self._stream:
self._stop_flag.set()
self.playing = False
self._volume = 0.0
self._target_volume = 0.0
self._pos = 0
self._paused = False
def set_target_volume(self, volume):
with self._lock:
self._target_volume = max(0.0, min(volume, 1.0))
def update_audio_system(listener, sources, debug=False):
for source in sources:
dist = np.linalg.norm(listener.position - source.position)
if dist <= source.emit_radius:
attenuation = 1.0 - dist / source.emit_radius
volume = source.base_volume * attenuation
if debug:
print(f"[Audio] Distance: {dist:.2f}, Volume: {volume:.2f}")
if not source.playing:
source.play(volume)
else:
if source._paused:
# Jeśli był pauzowany, startuj od nowa z głośnością 0 i fade-in
source.stop()
source.play(volume)
else:
source.set_target_volume(volume)
else:
if source.playing:
source.set_target_volume(0.0)
# Poczekaj, aż głośność spadnie prawie do zera, wtedy pauzuj
if source._volume <= 0.01 and not source._paused:
if debug:
print(f"[PAUSE] {source.sound_path} - volume zero")
source.pause()

113
my3dengine/camera.py Normal file
View File

@@ -0,0 +1,113 @@
from OpenGL.GL import *
from .key import Key
from .mesh import Mesh
import sdl2
import numpy as np
import ctypes
def normalize(v):
norm = np.linalg.norm(v)
if norm == 0:
return v
return v / norm
def look_at(eye, center, up):
f = normalize(center - eye)
s = normalize(np.cross(f, up))
u = np.cross(s, f)
mat = np.identity(4, dtype=np.float32)
mat[0, :3] = s
mat[1, :3] = u
mat[2, :3] = -f
mat[:3, 3] = -np.dot(mat[:3, :3], eye)
return mat
def perspective(fov, aspect, near, far):
f = 1.0 / np.tan(np.radians(fov) / 2)
mat = np.zeros((4, 4), dtype=np.float32)
mat[0, 0] = f / aspect
mat[1, 1] = f
mat[2, 2] = (far + near) / (near - far)
mat[2, 3] = (2 * far * near) / (near - far)
mat[3, 2] = -1
return mat
class Camera:
def __init__(self, position, target, up, fov, aspect, near=0.1, far=100.0):
self.position = np.array(position, dtype=np.float32)
self.target = np.array(target, dtype=np.float32)
self.up = np.array(up, dtype=np.float32)
self.fov = fov
self.aspect = aspect
self.near = near
self.far = far
self.move_speed = 5.0
self.mouse_sensitivity = 0.003
@property
def view_matrix(self):
return look_at(self.position, self.target, self.up)
@property
def projection_matrix(self):
return perspective(self.fov, self.aspect, self.near, self.far)
@property
def view_proj_matrix(self):
return self.projection_matrix @ self.view_matrix
def move(self, delta):
self.position += delta
self.target += delta
def handle_input(self, dt, window):
speed = self.move_speed * dt
forward = normalize(self.target - self.position)
right = normalize(np.cross(forward, self.up))
if window.is_key_pressed(Key.W):
self.move(forward * speed)
if window.is_key_pressed(Key.S):
self.move(-forward * speed)
if window.is_key_pressed(Key.A):
self.move(-right * speed)
if window.is_key_pressed(Key.D):
self.move(right * speed)
# Dodane ruchy w górę/dół
if window.is_key_pressed(Key.SPACE): # Spacja - do góry
self.move(self.up * speed)
if window.is_key_pressed(Key.LCTRL): # Lewy Ctrl - w dół
self.move(-self.up * speed)
if getattr(window, "mouse_locked", True):
xrel = ctypes.c_int()
yrel = ctypes.c_int()
sdl2.SDL_GetRelativeMouseState(ctypes.byref(xrel), ctypes.byref(yrel))
yaw = -xrel.value * self.mouse_sensitivity
pitch = -yrel.value * self.mouse_sensitivity
def rotate(v, axis, angle):
axis = normalize(axis)
cos_a = np.cos(angle)
sin_a = np.sin(angle)
return v * cos_a + np.cross(axis, v) * sin_a + axis * np.dot(axis, v) * (1 - cos_a)
offset = self.target - self.position
offset = rotate(offset, self.up, yaw)
right = normalize(np.cross(offset, self.up))
offset = rotate(offset, right, pitch)
self.target = self.position + offset
def set_uniforms(self, shader_program):
program_id = shader_program.program # ID programu
loc_v = glGetUniformLocation(program_id, "uView")
loc_p = glGetUniformLocation(program_id, "uProjection")
glUniformMatrix4fv(loc_v, 1, GL_FALSE, self.view_matrix.T)
glUniformMatrix4fv(loc_p, 1, GL_FALSE, self.projection_matrix.T)

151
my3dengine/core.py Normal file
View File

@@ -0,0 +1,151 @@
import sdl2
import sdl2.ext
from OpenGL.GL import *
import time
from sdl2 import SDL_GL_SetSwapInterval
from .key import Key
import numpy as np
import OpenGL.GL
class Vector3:
def __init__(self, x=0, y=0, z=0):
self.x = float(x)
self.y = float(y)
self.z = float(z)
def __add__(self, other):
return Vector3(self.x + other.x, self.y + other.y, self.z + other.z)
def __sub__(self, other):
return Vector3(self.x - other.x, self.y - other.y, self.z - other.z)
def __mul__(self, scalar):
return Vector3(self.x * scalar, self.y * scalar, self.z * scalar)
def to_list(self):
return [self.x, self.y, self.z]
def __repr__(self):
return f"Vector3({self.x}, {self.y}, {self.z})"
def debug_init():
@staticmethod
def init():
# Włącz przezroczystość (alpha blending)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# Włącz test głębokości (żeby obiekty były dobrze rysowane w 3D)
glEnable(GL_DEPTH_TEST)
glDepthFunc(GL_LESS) # Domyślnie można zostawić, ale warto jawnie ustawić
# Włącz odrzucanie tylnych ścianek (culling)
glEnable(GL_CULL_FACE)
glCullFace(GL_BACK) # Możesz też użyć GL_FRONT, jeśli robisz np. odbicia
glFrontFace(GL_CCW) # Counter-clockwise - domyślnie standard OpenGL
# Głębia na 1.0 - domyślnie, ale dobrze ustawić jawnie
glClearDepth(1.0)
# V-Sync
SDL_GL_SetSwapInterval(1) # 1 = włączony VSync, 0 = wyłączony (lepszy FPS, może tearing)
# Gładkie linie i wielokąty (opcjonalne, zależnie od potrzeb)
glEnable(GL_LINE_SMOOTH)
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
glEnable(GL_POLYGON_SMOOTH)
glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST)
# Antyaliasing multisampling (jeśli masz MSAA ustawione w SDL_GL)
glEnable(GL_MULTISAMPLE)
def normalize(v):
norm = np.linalg.norm(v)
if norm == 0:
return v
return v / norm
@staticmethod
def init():
glEnable(GL_BLEND)
glEnable(GL_DEPTH_TEST)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable(GL_CULL_FACE)
glFrontFace(GL_CCW)
glClearDepth(1.0)
SDL_GL_SetSwapInterval(1)
@staticmethod
def get_fullscreen():
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) # upewniamy się że SDL wystartował
mode = sdl2.SDL_DisplayMode()
if sdl2.SDL_GetDesktopDisplayMode(0, mode) != 0:
raise RuntimeError("Nie udało się pobrać trybu ekranu")
return mode.w, mode.h
class Window:
def __init__(self, title, width, height):
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
self.width = width
self.height = height
self.window = sdl2.SDL_CreateWindow(
title.encode("utf-8"),
sdl2.SDL_WINDOWPOS_CENTERED,
sdl2.SDL_WINDOWPOS_CENTERED,
width,
height,
sdl2.SDL_WINDOW_OPENGL | sdl2.SDL_WINDOW_SHOWN,
)
self.context = sdl2.SDL_GL_CreateContext(self.window)
glViewport(0, 0, width, height)
self.mouse_locked = True
sdl2.SDL_SetRelativeMouseMode(sdl2.SDL_TRUE)
sdl2.SDL_ShowCursor(sdl2.SDL_DISABLE)
def is_key_pressed(self, key: Key) -> bool:
keys = sdl2.SDL_GetKeyboardState(None)
return keys[key] != 0
def toggle_mouse_lock(self):
self.mouse_locked = not self.mouse_locked
if self.mouse_locked:
sdl2.SDL_SetRelativeMouseMode(sdl2.SDL_TRUE)
sdl2.SDL_ShowCursor(sdl2.SDL_DISABLE)
else:
sdl2.SDL_SetRelativeMouseMode(sdl2.SDL_FALSE)
sdl2.SDL_ShowCursor(sdl2.SDL_ENABLE)
def prepare_frame(self):
glViewport(0, 0, self.width, self.height)
glClearColor(0.1, 0.1, 0.1, 1.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glEnable(GL_DEPTH_TEST)
def run(self, update_callback=None, cameras=[]):
last_time = time.time()
event = sdl2.SDL_Event()
running = True
while running:
while sdl2.SDL_PollEvent(event):
if event.type == sdl2.SDL_QUIT:
running = False
elif event.type == sdl2.SDL_KEYDOWN:
if event.key.keysym.scancode == sdl2.SDL_SCANCODE_ESCAPE:
self.toggle_mouse_lock()
now = time.time()
dt = now - last_time
last_time = now
self.prepare_frame()
if update_callback:
update_callback(dt, self)
sdl2.SDL_GL_SwapWindow(self.window)
glFlush()
sdl2.SDL_DestroyWindow(self.window)
sdl2.SDL_Quit()

77
my3dengine/key.py Normal file
View File

@@ -0,0 +1,77 @@
# my3dengine/key.py
import sdl2
from enum import IntEnum
class Key(IntEnum):
# Strzałki
UP = sdl2.SDL_SCANCODE_UP
DOWN = sdl2.SDL_SCANCODE_DOWN
LEFT = sdl2.SDL_SCANCODE_LEFT
RIGHT = sdl2.SDL_SCANCODE_RIGHT
# Klawisze literowe
A = sdl2.SDL_SCANCODE_A
B = sdl2.SDL_SCANCODE_B
C = sdl2.SDL_SCANCODE_C
D = sdl2.SDL_SCANCODE_D
E = sdl2.SDL_SCANCODE_E
F = sdl2.SDL_SCANCODE_F
G = sdl2.SDL_SCANCODE_G
H = sdl2.SDL_SCANCODE_H
I = sdl2.SDL_SCANCODE_I
J = sdl2.SDL_SCANCODE_J
K = sdl2.SDL_SCANCODE_K
L = sdl2.SDL_SCANCODE_L
M = sdl2.SDL_SCANCODE_M
N = sdl2.SDL_SCANCODE_N
O = sdl2.SDL_SCANCODE_O
P = sdl2.SDL_SCANCODE_P
Q = sdl2.SDL_SCANCODE_Q
R = sdl2.SDL_SCANCODE_R
S = sdl2.SDL_SCANCODE_S
T = sdl2.SDL_SCANCODE_T
U = sdl2.SDL_SCANCODE_U
V = sdl2.SDL_SCANCODE_V
W = sdl2.SDL_SCANCODE_W
X = sdl2.SDL_SCANCODE_X
Y = sdl2.SDL_SCANCODE_Y
Z = sdl2.SDL_SCANCODE_Z
# Klawisze numeryczne
NUMBER_0 = sdl2.SDL_SCANCODE_0
NUMBER_1 = sdl2.SDL_SCANCODE_1
NUMBER_2 = sdl2.SDL_SCANCODE_2
NUMBER_3 = sdl2.SDL_SCANCODE_3
NUMBER_4 = sdl2.SDL_SCANCODE_4
NUMBER_5 = sdl2.SDL_SCANCODE_5
NUMBER_6 = sdl2.SDL_SCANCODE_6
NUMBER_7 = sdl2.SDL_SCANCODE_7
NUMBER_8 = sdl2.SDL_SCANCODE_8
NUMBER_9 = sdl2.SDL_SCANCODE_9
# Klawisze funkcyjne
F1 = sdl2.SDL_SCANCODE_F1
F2 = sdl2.SDL_SCANCODE_F2
F3 = sdl2.SDL_SCANCODE_F3
F4 = sdl2.SDL_SCANCODE_F4
F5 = sdl2.SDL_SCANCODE_F5
F6 = sdl2.SDL_SCANCODE_F6
F7 = sdl2.SDL_SCANCODE_F7
F8 = sdl2.SDL_SCANCODE_F8
F9 = sdl2.SDL_SCANCODE_F9
F10 = sdl2.SDL_SCANCODE_F10
F11 = sdl2.SDL_SCANCODE_F11
F12 = sdl2.SDL_SCANCODE_F12
# Inne ważne
SPACE = sdl2.SDL_SCANCODE_SPACE
ESCAPE = sdl2.SDL_SCANCODE_ESCAPE
RETURN = sdl2.SDL_SCANCODE_RETURN
TAB = sdl2.SDL_SCANCODE_TAB
LSHIFT = sdl2.SDL_SCANCODE_LSHIFT
RSHIFT = sdl2.SDL_SCANCODE_RSHIFT
LCTRL = sdl2.SDL_SCANCODE_LCTRL
RCTRL = sdl2.SDL_SCANCODE_RCTRL
ALT = sdl2.SDL_SCANCODE_LALT
BACKSPACE = sdl2.SDL_SCANCODE_BACKSPACE
CAPSLOCK = sdl2.SDL_SCANCODE_CAPSLOCK

489
my3dengine/mesh.py Normal file
View File

@@ -0,0 +1,489 @@
from OpenGL.GL import *
import numpy as np
from .shader import basic_shader
from PIL import Image
import ctypes
import math
import time
_default_shader = None
_last_bound_shader = [None]
_last_bound_vao = [None]
def get_default_shader():
global _default_shader
if _default_shader is None:
_default_shader = basic_shader()
_default_shader.uniforms = {
"uModel": glGetUniformLocation(_default_shader.program, "uModel"),
"uUVTiling": glGetUniformLocation(_default_shader.program, "uUVTiling"),
"uUVOffset": glGetUniformLocation(_default_shader.program, "uUVOffset"),
"uTexture": glGetUniformLocation(_default_shader.program, "uTexture"),
"useTexture": glGetUniformLocation(_default_shader.program, "useTexture"),
}
return _default_shader
def safe_use_shader(shader):
if _last_bound_shader[0] != shader:
glUseProgram(shader.program)
_last_bound_shader[0] = shader
def safe_bind_vao(vao):
if _last_bound_vao[0] != vao:
glBindVertexArray(vao)
_last_bound_vao[0] = vao
def get_model_matrix(position, scale):
model = np.identity(4, dtype=np.float32)
model[0, 0], model[1, 1], model[2, 2] = scale
model[:3, 3] = position # poprawione na kolumnę 3
return model
class Mesh:
def __init__(self, vertices, shader=None):
self.shader = shader or get_default_shader()
if not hasattr(self.shader, "uniforms"):
self.shader.uniforms = {
"uModel": glGetUniformLocation(self.shader.program, "uModel"),
"uUVTiling": glGetUniformLocation(self.shader.program, "uUVTiling"),
"uUVOffset": glGetUniformLocation(self.shader.program, "uUVOffset"),
"uTexture": glGetUniformLocation(self.shader.program, "uTexture"),
"useTexture": glGetUniformLocation(self.shader.program, "useTexture"),
}
self.vertex_count = len(vertices) // 8
self.position = np.zeros(3, dtype=np.float32)
self.scale = np.ones(3, dtype=np.float32)
self.texture = None
self.uv_tiling = np.ones(2, dtype=np.float32)
self.uv_offset = np.zeros(2, dtype=np.float32)
self._last_position = np.array([np.nan, np.nan, np.nan], dtype=np.float32)
self._last_scale = np.array([np.nan, np.nan, np.nan], dtype=np.float32)
self._uniforms_uploaded_once = False
self._init_buffers(vertices)
def copy(self):
new = Mesh.__new__(Mesh)
new.__dict__ = self.__dict__.copy()
new.position = self.position.copy()
new.scale = self.scale.copy()
new._last_position = np.array([np.nan, np.nan, np.nan], dtype=np.float32)
new._last_scale = np.array([np.nan, np.nan, np.nan], dtype=np.float32)
self._uniforms_uploaded_once = False
return new
def is_in_fov(self, cam_pos, cam_dir, fov_deg):
half_fov_rad = math.radians(fov_deg * 1.2)
cam_dir = cam_dir / np.linalg.norm(cam_dir)
to_obj = self.position - cam_pos
dist = np.linalg.norm(to_obj)
if dist == 0:
return True
to_obj_dir = to_obj / dist
angle = math.acos(np.clip(np.dot(cam_dir, to_obj_dir), -1, 1))
return angle <= half_fov_rad
def _init_buffers(self, vertices):
data = np.array(vertices, dtype=np.float32)
self.vao = glGenVertexArrays(1)
self.vbo = glGenBuffers(1)
glBindVertexArray(self.vao)
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
glBufferData(GL_ARRAY_BUFFER, data.nbytes, data, GL_STATIC_DRAW)
stride = 8 * 4
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(0))
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(12))
glEnableVertexAttribArray(1)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(24))
glEnableVertexAttribArray(2)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
def set_uv_transform(self, tiling=(1.0, 1.0), offset=(0.0, 0.0)):
self.uv_tiling[:] = tiling
self.uv_offset[:] = offset
def upload_uniforms(self):
if (not self._uniforms_uploaded_once or
not (np.array_equal(self.position, self._last_position) and
np.array_equal(self.scale, self._last_scale))):
model = get_model_matrix(self.position, self.scale)
uniforms = self.shader.uniforms
glUniformMatrix4fv(uniforms["uModel"], 1, GL_FALSE, model.T)
glUniform2fv(uniforms["uUVTiling"], 1, self.uv_tiling)
glUniform2fv(uniforms["uUVOffset"], 1, self.uv_offset)
self._last_position = self.position.copy()
self._last_scale = self.scale.copy()
self._uniforms_uploaded_once = True
def draw(self, cam_pos=None, cam_dir=None, fov_deg=70, view_proj_matrix=None, debug=False):
if debug:
print(f"Draw called for mesh at {self.position}")
if cam_pos is not None:
dist = np.linalg.norm(self.position - cam_pos)
if dist > 600: # ustaw granicę zależnie od sceny
return
# if cam_pos is not None and cam_dir is not None:
# if not self.is_in_fov(cam_pos, cam_dir, fov_deg + 50): # +10° tolerancji
# return
safe_use_shader(self.shader)
self.upload_uniforms()
uniforms = self.shader.uniforms
if self.texture:
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, self.texture)
glUniform1i(uniforms["uTexture"], 0)
glUniform1i(uniforms["useTexture"], 1)
else:
glUniform1i(uniforms["useTexture"], 0)
safe_bind_vao(self.vao)
glDrawArrays(GL_TRIANGLES, 0, self.vertex_count)
if debug:
print(f"[DEBUG] Mesh at {self.position} został narysowany.")
def set_texture(self, path):
image = Image.open(path).transpose(Image.FLIP_TOP_BOTTOM).convert('RGBA')
img_data = np.array(image, dtype=np.uint8)
if self.texture:
glDeleteTextures(1, [self.texture])
self.texture = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, self.texture)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, 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)
def set_position(self, x, y, z):
self.position[:] = [x, y, z]
def set_scale(self, x, y, z):
self.scale[:] = [x, y, z]
def scale_uniform(self, factor):
self.scale[:] = factor
def destroy(self):
if hasattr(self, 'vao') and glDeleteVertexArrays:
glDeleteVertexArrays(1, [self.vao])
glDeleteBuffers(1, [self.vbo])
if self.texture and glDeleteTextures:
glDeleteTextures(1, [self.texture])
# =========================
# ======== models =========
# =========================
@staticmethod
def from_obj(path, color=(1, 1, 1)):
positions = []
texcoords = []
faces = []
with open(path, "r") as f:
for line in f:
if line.startswith("v "): # vertex position
parts = line.strip().split()
positions.append(tuple(map(float, parts[1:4])))
elif line.startswith("vt "): # texture coordinate
parts = line.strip().split()
texcoords.append(tuple(map(float, parts[1:3])))
elif line.startswith("f "): # face
parts = line.strip().split()[1:]
face = []
for p in parts:
vals = p.split('/')
vi = int(vals[0]) - 1
ti = int(vals[1]) - 1 if len(vals) > 1 and vals[1] else 0
face.append((vi, ti))
faces.append(face)
verts = []
for face in faces:
if len(face) >= 3:
for i in range(1, len(face) - 1):
for idx in [0, i, i + 1]:
vi, ti = face[idx]
pos = positions[vi]
uv = texcoords[ti] if texcoords else (0.0, 0.0)
verts.extend([*pos, *color, *uv])
return Mesh(verts, basic_shader())
@staticmethod
def cube(color=(1, 1, 1)):
# Pozycje wierzchołków sześcianu
p = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5), # front
(-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5)] # back
# UV mapowanie na jedną ścianę (powtarzane dla każdej)
uv = [(0, 0), (1, 0), (1, 1), (0, 1)]
# Indeksy do rysowania ścian (każda ściana: 2 trójkąty)
faces = [
(0, 1, 2, 3), # front
(5, 4, 7, 6), # back
(4, 0, 3, 7), # left
(1, 5, 6, 2), # right
(3, 2, 6, 7), # top
(4, 5, 1, 0) # bottom
]
verts = []
for face in faces:
idx = [face[0], face[1], face[2], face[0], face[2], face[3]]
uv_idx = [0, 1, 2, 0, 2, 3]
for i in range(6):
pos = p[idx[i]]
tex = uv[uv_idx[i]]
verts.extend([*pos, *color, *tex])
return Mesh(verts, basic_shader())
@staticmethod
def sphere(radius=0.5, lat_segments=16, lon_segments=16, color=(1, 1, 1)):
verts = []
for i in range(lat_segments):
theta1 = np.pi * (i / lat_segments - 0.5)
theta2 = np.pi * ((i + 1) / lat_segments - 0.5)
for j in range(lon_segments):
phi1 = 2 * np.pi * (j / lon_segments)
phi2 = 2 * np.pi * ((j + 1) / lon_segments)
def get_pos(theta, phi):
return (
radius * np.cos(theta) * np.cos(phi),
radius * np.sin(theta),
radius * np.cos(theta) * np.sin(phi)
)
# Wierzchołki
p1 = get_pos(theta1, phi1)
p2 = get_pos(theta2, phi1)
p3 = get_pos(theta2, phi2)
p4 = get_pos(theta1, phi2)
# UV mapping (prosty sferyczny)
uv1 = (j / lon_segments, i / lat_segments)
uv2 = (j / lon_segments, (i + 1) / lat_segments)
uv3 = ((j + 1) / lon_segments, (i + 1) / lat_segments)
uv4 = ((j + 1) / lon_segments, i / lat_segments)
# Trójkąty
for vtx, uv in zip([p1, p2, p3], [uv1, uv2, uv3]):
verts.extend([*vtx, *color, *uv])
for vtx, uv in zip([p1, p3, p4], [uv1, uv3, uv4]):
verts.extend([*vtx, *color, *uv])
return Mesh(verts, basic_shader())
@staticmethod
def capsule(radius=0.25, height=1.0, segments=16, color=(1, 1, 1)):
verts = []
half = height / 2
# === Cylinder środkowy ===
for j in range(segments):
theta1 = 2 * np.pi * (j / segments)
theta2 = 2 * np.pi * ((j + 1) / segments)
x1, z1 = np.cos(theta1), np.sin(theta1)
x2, z2 = np.cos(theta2), np.sin(theta2)
p1 = (radius * x1, -half, radius * z1)
p2 = (radius * x1, half, radius * z1)
p3 = (radius * x2, half, radius * z2)
p4 = (radius * x2, -half, radius * z2)
uv = [(j / segments, 0.0), (j / segments, 0.5), ((j + 1) / segments, 0.5), ((j + 1) / segments, 0.0)]
for vtx, tex in zip([p1, p2, p3], [uv[0], uv[1], uv[2]]):
verts.extend([*vtx, *color, *tex])
for vtx, tex in zip([p1, p3, p4], [uv[0], uv[2], uv[3]]):
verts.extend([*vtx, *color, *tex])
# === Półsfera górna ===
for i in range(segments // 2):
theta1 = (np.pi / 2) * (i / (segments // 2))
theta2 = (np.pi / 2) * ((i + 1) / (segments // 2))
for j in range(segments):
phi1 = 2 * np.pi * (j / segments)
phi2 = 2 * np.pi * ((j + 1) / segments)
def pos(theta, phi):
return (
radius * np.cos(theta) * np.cos(phi),
radius * np.sin(theta) + half,
radius * np.cos(theta) * np.sin(phi)
)
p1 = pos(theta1, phi1)
p2 = pos(theta2, phi1)
p3 = pos(theta2, phi2)
p4 = pos(theta1, phi2)
uv1 = (j / segments, 0.5 + (i / (segments * 2)))
uv2 = (j / segments, 0.5 + ((i + 1) / (segments * 2)))
uv3 = ((j + 1) / segments, 0.5 + ((i + 1) / (segments * 2)))
uv4 = ((j + 1) / segments, 0.5 + (i / (segments * 2)))
for vtx, tex in zip([p1, p2, p3], [uv1, uv2, uv3]):
verts.extend([*vtx, *color, *tex])
for vtx, tex in zip([p1, p3, p4], [uv1, uv3, uv4]):
verts.extend([*vtx, *color, *tex])
# === Półsfera dolna ===
for i in range(segments // 2):
theta1 = (np.pi / 2) * (i / (segments // 2))
theta2 = (np.pi / 2) * ((i + 1) / (segments // 2))
for j in range(segments):
phi1 = 2 * np.pi * (j / segments)
phi2 = 2 * np.pi * ((j + 1) / segments)
def pos(theta, phi):
return (
radius * np.cos(theta) * np.cos(phi),
-radius * np.sin(theta) - half,
radius * np.cos(theta) * np.sin(phi)
)
p1 = pos(theta1, phi1)
p2 = pos(theta2, phi1)
p3 = pos(theta2, phi2)
p4 = pos(theta1, phi2)
uv1 = (j / segments, 0.5 - (i / (segments * 2)))
uv2 = (j / segments, 0.5 - ((i + 1) / (segments * 2)))
uv3 = ((j + 1) / segments, 0.5 - ((i + 1) / (segments * 2)))
uv4 = ((j + 1) / segments, 0.5 - (i / (segments * 2)))
# UWAGA: zamieniona kolejność rysowania — PRAWIDŁOWY winding
for vtx, tex in zip([p1, p3, p2], [uv1, uv3, uv2]):
verts.extend([*vtx, *color, *tex])
for vtx, tex in zip([p1, p4, p3], [uv1, uv4, uv3]):
verts.extend([*vtx, *color, *tex])
return Mesh(verts, basic_shader())
@staticmethod
def plane(size=1.0, color=(1, 1, 1)):
hs = size / 2
positions = [(-hs, 0, -hs), (hs, 0, -hs), (hs, 0, hs), (-hs, 0, hs)]
uvs = [(0, 0), (1, 0), (1, 1), (0, 1)]
indices = [(0, 1, 2), (0, 2, 3)] # front
back_indices = [(2, 1, 0), (3, 2, 0)] # back side (odwrócone)
verts = []
for face in indices + back_indices:
for idx in face:
verts.extend([*positions[idx], *color, *uvs[idx]])
return Mesh(verts, basic_shader())
@staticmethod
def water(
size=10.0,
resolution=64,
wave_speed=0.03,
wave_height=0.1,
wave_scale=(0.3, 0.4),
second_wave=True,
color=(0.2, 0.5, 1.0),
backface=True
):
half = size / 2
step = size / resolution
verts = []
for z in range(resolution):
for x in range(resolution):
x0 = -half + x * step
x1 = x0 + step
z0 = -half + z * step
z1 = z0 + step
u0, u1 = x / resolution, (x + 1) / resolution
v0, v1 = z / resolution, (z + 1) / resolution
pos = [(x0, 0.0, z0), (x1, 0.0, z0), (x1, 0.0, z1), (x0, 0.0, z1)]
uv = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)]
# Indeksy dla frontu i ewentualnie backface
indices = [(0, 1, 2), (0, 2, 3)]
if backface:
indices += [(2, 1, 0), (3, 2, 0)]
for a, b, c in indices:
for i in [a, b, c]:
verts.extend([*pos[i], *color, *uv[i]])
mesh = Mesh(verts, basic_shader())
mesh._water_size = size
mesh._water_res = resolution
mesh._water_time = 0.0
mesh._wave_speed = wave_speed
mesh._wave_height = wave_height
mesh._wave_scale = wave_scale
mesh._second_wave = second_wave
mesh._water_verts = np.array(verts, dtype=np.float32).reshape(-1, 8)
# Wysokości bazowe (y)
mesh._water_initial_y = mesh._water_verts[:, 1].copy()
# Dynamiczny buffer
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo)
glBufferData(GL_ARRAY_BUFFER, mesh._water_verts.nbytes, mesh._water_verts.flatten(), GL_DYNAMIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, 0)
def update_water():
mesh._water_time += mesh._wave_speed
verts = mesh._water_verts
X = verts[:, 0]
Z = verts[:, 2]
# Fale
verts[:, 1] = (
np.sin(X * mesh._wave_scale[0] + mesh._water_time) +
(np.cos(Z * mesh._wave_scale[1] + mesh._water_time) if mesh._second_wave else 0.0)
) * (mesh._wave_height * 0.5)
glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo)
glBufferSubData(GL_ARRAY_BUFFER, 0, verts.nbytes, verts.flatten())
glBindBuffer(GL_ARRAY_BUFFER, 0)
mesh.update_water = update_water
return mesh
def update_water(self):
if not hasattr(self, '_original_vertices'):
return
verts = self._original_vertices.copy()
t = time.time() - self._start_time
for i in range(0, len(verts), 8):
x = verts[i]
z = verts[i + 2]
verts[i + 1] = math.sin(x * 0.5 + t) * 0.2 + math.cos(z * 0.5 + t) * 0.2
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
glBufferSubData(GL_ARRAY_BUFFER, 0, verts.nbytes, verts)
glBindBuffer(GL_ARRAY_BUFFER, 0)

131
my3dengine/shader.py Normal file
View File

@@ -0,0 +1,131 @@
from OpenGL.GL import *
ui_vertex_src = """
#version 330 core
layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aUV;
out vec2 vUV;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
vUV = aUV;
}
"""
ui_fragment_src = """
#version 330 core
in vec2 vUV;
uniform sampler2D uTexture;
uniform vec4 uColor;
uniform int useTexture;
out vec4 FragColor;
void main() {
if (useTexture == 1)
FragColor = texture(uTexture, vUV) * uColor;
else
FragColor = uColor;
}
"""
basic_vertex_src = """
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aUV;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
uniform vec2 uUVTiling;
uniform vec2 uUVOffset;
out vec3 vColor;
out vec2 vUV;
void main() {
gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
vColor = aColor;
vUV = aUV * uUVTiling + uUVOffset;
}
"""
basic_fragment_src = """
#version 330 core
in vec3 vColor;
in vec2 vUV;
uniform sampler2D uTexture;
uniform int useTexture;
out vec4 FragColor;
void main() {
if (useTexture == 1)
FragColor = texture(uTexture, vUV);
else
FragColor = vec4(vColor, 1.0);
}
"""
def compile_shader(src, shader_type):
shader = glCreateShader(shader_type)
glShaderSource(shader, src)
glCompileShader(shader)
if glGetShaderiv(shader, GL_COMPILE_STATUS) != GL_TRUE:
error = glGetShaderInfoLog(shader).decode()
raise RuntimeError(f"Shader compile error: {error}")
return shader
def create_shader(vertex_src, fragment_src):
vert = compile_shader(vertex_src, GL_VERTEX_SHADER)
frag = compile_shader(fragment_src, GL_FRAGMENT_SHADER)
program = glCreateProgram()
glAttachShader(program, vert)
glAttachShader(program, frag)
glLinkProgram(program)
if glGetProgramiv(program, GL_LINK_STATUS) != GL_TRUE:
error = glGetProgramInfoLog(program).decode()
raise RuntimeError(f"Program link error: {error}")
glDeleteShader(vert)
glDeleteShader(frag)
return program
class ShaderProgram:
def __init__(self, vertex_src, fragment_src):
self.program = create_shader(vertex_src, fragment_src)
self.uniform_locations = {}
def use(self):
glUseProgram(self.program)
def get_uniform_location(self, name):
if name not in self.uniform_locations:
self.uniform_locations[name] = glGetUniformLocation(self.program, name)
return self.uniform_locations[name]
def set_uniform_1i(self, name, value):
glUniform1i(self.get_uniform_location(name), value)
def set_uniform_1f(self, name, value):
glUniform1f(self.get_uniform_location(name), value)
def set_uniform_vec2(self, name, vec):
glUniform2fv(self.get_uniform_location(name), 1, vec)
def set_uniform_vec3(self, name, vec):
glUniform3fv(self.get_uniform_location(name), 1, vec)
def set_uniform_mat4(self, name, mat, transpose=False):
glUniformMatrix4fv(self.get_uniform_location(name), 1, transpose, mat)
def create_ui_shader():
return ShaderProgram(ui_vertex_src, ui_fragment_src)
def basic_shader():
return ShaderProgram(basic_vertex_src, basic_fragment_src)

84
my3dengine/water.py Normal file
View File

@@ -0,0 +1,84 @@
import numpy as np
from OpenGL.GL import *
import ctypes
import time
class WaterMesh:
def __init__(self, size=10.0, resolution=100):
self.size = size
self.resolution = resolution
self.vertices = self.generate_vertices()
self.vertex_count = len(self.vertices) // 8
self.vao = glGenVertexArrays(1)
self.vbo = glGenBuffers(1)
glBindVertexArray(self.vao)
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
glBufferData(GL_ARRAY_BUFFER, self.vertices.nbytes, self.vertices, GL_DYNAMIC_DRAW)
stride = 8 * 4
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(0)) # pos
glEnableVertexAttribArray(0)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(12)) # color
glEnableVertexAttribArray(1)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(24)) # uv
glEnableVertexAttribArray(2)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
self.start_time = time.time()
def generate_vertices(self):
step = self.size / self.resolution
verts = []
for z in range(self.resolution):
for x in range(self.resolution):
# cztery rogi jednego kwadratu
x0 = x * step - self.size / 2
x1 = (x + 1) * step - self.size / 2
z0 = z * step - self.size / 2
z1 = (z + 1) * step - self.size / 2
uv0 = (x / self.resolution, z / self.resolution)
uv1 = ((x + 1) / self.resolution, z / self.resolution)
uv2 = ((x + 1) / self.resolution, (z + 1) / self.resolution)
uv3 = (x / self.resolution, (z + 1) / self.resolution)
color = (0.2, 0.4, 1.0)
# dwa trójkąty
verts += [x0, 0, z0, *color, *uv0]
verts += [x1, 0, z0, *color, *uv1]
verts += [x1, 0, z1, *color, *uv2]
verts += [x0, 0, z0, *color, *uv0]
verts += [x1, 0, z1, *color, *uv2]
verts += [x0, 0, z1, *color, *uv3]
return np.array(verts, dtype=np.float32)
def update(self):
time_now = time.time() - self.start_time
verts = self.vertices
for i in range(0, len(verts), 8):
x = verts[i]
z = verts[i + 2]
verts[i + 1] = np.sin(x * 0.5 + time_now) * 0.2 + np.cos(z * 0.5 + time_now) * 0.2
glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
glBufferSubData(GL_ARRAY_BUFFER, 0, verts.nbytes, verts)
glBindBuffer(GL_ARRAY_BUFFER, 0)
def draw(self, shader):
shader.use()
glBindVertexArray(self.vao)
glDrawArrays(GL_TRIANGLES, 0, self.vertex_count)
glBindVertexArray(0)
def destroy(self):
glDeleteVertexArrays(1, [self.vao])
glDeleteBuffers(1, [self.vbo])