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

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)