Compare commits
2 Commits
c572a34cb4
...
d2e09eb22c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2e09eb22c | ||
| e5591e7dc8 |
28
.gitignore
vendored
28
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
# ---> Python
|
||||
>>>>>>> c572a34cb4ba6d74ef88cda07e35e64b2ee5ab36
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -168,9 +171,34 @@ cython_debug/
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
<<<<<<< HEAD
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the enitre vscode folder
|
||||
# .vscode/
|
||||
|
||||
=======
|
||||
>>>>>>> c572a34cb4ba6d74ef88cda07e35e64b2ee5ab36
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
<<<<<<< HEAD
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
=======
|
||||
>>>>>>> c572a34cb4ba6d74ef88cda07e35e64b2ee5ab36
|
||||
|
||||
23
LICENSE
23
LICENSE
@@ -1,3 +1,25 @@
|
||||
<<<<<<< HEAD
|
||||
Fable3D License Agreement
|
||||
|
||||
Copyright (c) 2025 Michał Lewandowski
|
||||
|
||||
All rights reserved.
|
||||
|
||||
You are granted a non-exclusive, non-transferable license to download, install, and use Fable3D for personal, educational, or commercial purposes.
|
||||
|
||||
You may NOT:
|
||||
- modify, adapt, translate, or create derivative works based on Fable3D,
|
||||
- distribute, sublicense, rent, lease, or sell copies of Fable3D or any derivative works,
|
||||
- reverse engineer, decompile, or disassemble the software.
|
||||
|
||||
Any unauthorized use, modification, or distribution is strictly prohibited and may result in legal action.
|
||||
|
||||
This license does NOT grant you any rights to access the source code or make changes to it.
|
||||
|
||||
---
|
||||
|
||||
If you want to request additional rights or commercial licenses, please contact: slawek.1q2w3e4r@gmail.com
|
||||
=======
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 mmichlol
|
||||
@@ -16,3 +38,4 @@ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE A
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
>>>>>>> c572a34cb4ba6d74ef88cda07e35e64b2ee5ab36
|
||||
|
||||
32
README.md
32
README.md
@@ -1,2 +1,34 @@
|
||||
# Fable3D
|
||||
|
||||
<<<<<<< HEAD
|
||||
**Fable3D** is a lightweight, modular 3D engine written in Python — designed for game developers, simulation creators, and interactive world builders. It combines the simplicity of Python with powerful 3D rendering capabilities through OpenGL.
|
||||
|
||||
> "Every game is a new story — start yours with Fable3D."
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- Custom scene graph structure
|
||||
- Support for 3D models (OBJ, FBX, custom formats)
|
||||
- Materials and GLSL shaders
|
||||
- Perspective and orthographic cameras
|
||||
- Lighting (point, directional, ambient)
|
||||
- Mesh system with textures and buffers
|
||||
- Input handling for mouse, keyboard, and controllers
|
||||
- Real-time engine with deltaTime and FPS management
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python `3.10+`
|
||||
- `PyOpenGL`
|
||||
- `numpy`
|
||||
|
||||
Install dependencies with:
|
||||
|
||||
```bash
|
||||
pip install Fable3D
|
||||
=======
|
||||
>>>>>>> c572a34cb4ba6d74ef88cda07e35e64b2ee5ab36
|
||||
|
||||
2
examples/assets/Untitled.mtl
Normal file
2
examples/assets/Untitled.mtl
Normal file
@@ -0,0 +1,2 @@
|
||||
# Blender 4.4.3 MTL File: 'None'
|
||||
# www.blender.org
|
||||
57
examples/assets/Untitled.obj
Normal file
57
examples/assets/Untitled.obj
Normal file
@@ -0,0 +1,57 @@
|
||||
# Blender 4.4.3
|
||||
# www.blender.org
|
||||
mtllib Untitled.mtl
|
||||
o Cube
|
||||
v -1.000000 -1.000000 1.000000
|
||||
v -1.000000 -1.000000 -1.000000
|
||||
v 1.000000 -1.000000 1.000000
|
||||
v 1.000000 -1.000000 -1.000000
|
||||
v -0.000000 4.229822 0.000000
|
||||
v -1.000000 3.229822 1.000000
|
||||
v -0.000000 4.229822 -0.000000
|
||||
v -1.000000 3.229822 -1.000000
|
||||
v 0.000000 4.229822 0.000000
|
||||
v 1.000000 3.229822 1.000000
|
||||
v 0.000000 4.229822 -0.000000
|
||||
v 1.000000 3.229822 -1.000000
|
||||
vn -0.5774 -0.5774 0.5774
|
||||
vn -0.5774 -0.5774 -0.5774
|
||||
vn 0.5774 -0.5774 0.5774
|
||||
vn 0.5774 -0.5774 -0.5774
|
||||
vn -0.3002 0.9054 0.3002
|
||||
vn -0.6507 0.3914 0.6507
|
||||
vn -0.3002 0.9054 -0.3002
|
||||
vn -0.6507 0.3914 -0.6507
|
||||
vn 0.3002 0.9054 0.3002
|
||||
vn 0.6507 0.3914 0.6507
|
||||
vn 0.3002 0.9054 -0.3002
|
||||
vn 0.6507 0.3914 -0.6507
|
||||
vt 0.750000 0.625000
|
||||
vt 0.750000 0.625000
|
||||
vt 0.375000 0.500000
|
||||
vt 0.577197 0.500000
|
||||
vt 0.577197 0.750000
|
||||
vt 0.375000 0.750000
|
||||
vt 0.375000 0.000000
|
||||
vt 0.577197 0.000000
|
||||
vt 0.577197 0.250000
|
||||
vt 0.375000 0.250000
|
||||
vt 0.125000 0.500000
|
||||
vt 0.125000 0.750000
|
||||
vt 0.875000 0.500000
|
||||
vt 0.875000 0.750000
|
||||
vt 0.625000 0.500000
|
||||
vt 0.625000 0.750000
|
||||
vt 0.577197 1.000000
|
||||
vt 0.375000 1.000000
|
||||
s 1
|
||||
f 11/1/11 7/1/7 5/2/5 9/1/9
|
||||
f 4/3/4 12/4/12 10/5/10 3/6/3
|
||||
f 1/7/1 6/8/6 8/9/8 2/10/2
|
||||
f 2/11/2 4/3/4 3/6/3 1/12/1
|
||||
f 2/10/2 8/9/8 12/4/12 4/3/4
|
||||
f 5/2/5 7/1/7 8/13/8 6/14/6
|
||||
f 7/1/7 11/1/11 12/15/12 8/13/8
|
||||
f 11/1/11 9/1/9 10/5/10 12/4/12
|
||||
f 9/1/9 5/2/5 6/14/6 10/16/10
|
||||
f 3/6/3 10/5/10 6/17/6 1/18/1
|
||||
BIN
examples/assets/bricksx64.png
Normal file
BIN
examples/assets/bricksx64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
examples/assets/dark-atmosphere-background-008-312381.mp3
Normal file
BIN
examples/assets/dark-atmosphere-background-008-312381.mp3
Normal file
Binary file not shown.
BIN
examples/assets/dark-atmosphere-background-009-312380.mp3
Normal file
BIN
examples/assets/dark-atmosphere-background-009-312380.mp3
Normal file
Binary file not shown.
BIN
examples/assets/sword-sound.mp3
Normal file
BIN
examples/assets/sword-sound.mp3
Normal file
Binary file not shown.
BIN
examples/assets/texture.png
Normal file
BIN
examples/assets/texture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 616 B |
BIN
examples/assets/water.png
Normal file
BIN
examples/assets/water.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
151
examples/game.py
Normal file
151
examples/game.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import my3dengine as m3d
|
||||
from OpenGL.GL import *
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
class Game:
|
||||
def __init__(self):
|
||||
self.WIDTH, self.HEIGHT = m3d.get_fullscreen()
|
||||
self.window = m3d.Window("3D Game", self.WIDTH, self.HEIGHT)
|
||||
self.keys = m3d.Key
|
||||
self.escape_was_pressed = False
|
||||
|
||||
self.sources = [
|
||||
m3d.AudioSource(position=[0, 0, 3], sound_path="assets/dark-atmosphere-background-009-312380.mp3", emit_radius=5, volume=1.0, loop=False, fade_duration=1.0),
|
||||
]
|
||||
|
||||
# FPS counter
|
||||
self.last_fps_time = time.time()
|
||||
self.frames = 0
|
||||
|
||||
self.camera = m3d.Camera(
|
||||
position=(0, 0, 3),
|
||||
target=(0, 0, 0),
|
||||
up=(0, 1, 0),
|
||||
fov=60,
|
||||
aspect=self.WIDTH / self.HEIGHT,
|
||||
)
|
||||
|
||||
self.listener = m3d.AudioListener(self.camera.position)
|
||||
|
||||
self.ui = m3d.UI(self.WIDTH, self.HEIGHT)
|
||||
|
||||
self.meshes = [
|
||||
m3d.Mesh.cube(),
|
||||
m3d.Mesh.sphere(),
|
||||
m3d.Mesh.capsule(),
|
||||
m3d.Mesh.cube(),
|
||||
m3d.Mesh.plane(),
|
||||
m3d.Mesh.from_obj("assets/untitled.obj"),
|
||||
]
|
||||
|
||||
# WODA
|
||||
self.water = m3d.Mesh.water(
|
||||
size=20.0,
|
||||
resolution=64, # 🧠 Mniejsze resolution = lepsze FPS
|
||||
wave_speed=0.01,
|
||||
wave_height=0.5,
|
||||
wave_scale=(0.4, 0.6),
|
||||
second_wave=True,
|
||||
color=(0.0, 0.4, 0.8),
|
||||
backface=True
|
||||
)
|
||||
self.water.set_position(15, 0, 15)
|
||||
self.water.set_texture("assets/water.png")
|
||||
self.water.set_uv_transform((10, 10), (0, 0))
|
||||
|
||||
# Pozycje
|
||||
self.meshes[0].set_position(0, 0, 0)
|
||||
self.meshes[1].set_position(0, 0, 5)
|
||||
self.meshes[2].set_position(2, 0, 0)
|
||||
self.meshes[3].set_position(-4, 0, 0)
|
||||
self.meshes[4].set_position(0, -4, 0)
|
||||
self.meshes[5].set_position(-8, 0, 0)
|
||||
|
||||
# Skale
|
||||
self.meshes[2].scale_uniform(2)
|
||||
self.meshes[3].set_scale(1, 0.2, 1)
|
||||
self.meshes[4].scale_uniform(15)
|
||||
self.meshes[5].scale_uniform(0.5)
|
||||
|
||||
# Tekstury
|
||||
for i in range(6):
|
||||
self.meshes[i].set_texture("assets/bricksx64.png")
|
||||
|
||||
# UV
|
||||
self.meshes[5].set_uv_transform(tiling=(10, 10), offset=(0.1, 0.1))
|
||||
self.meshes[4].set_uv_transform(tiling=(10, 10), offset=(0.1, 0.1))
|
||||
self.meshes[1].set_uv_transform(tiling=(5, 5), offset=(0.1, 0.1))
|
||||
self.meshes[2].set_uv_transform(tiling=(5, 5), offset=(0.1, 0.1))
|
||||
|
||||
# OpenGL ustawienia
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
glEnable(GL_CULL_FACE)
|
||||
glCullFace(GL_BACK)
|
||||
|
||||
def update(self, dt, window):
|
||||
self.camera.handle_input(dt, window)
|
||||
|
||||
glViewport(0, 0, self.WIDTH, self.HEIGHT)
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
self.listener.position[:] = self.camera.position
|
||||
|
||||
cam_pos = self.camera.position
|
||||
cam_dir = m3d.normalize(self.camera.target - cam_pos)
|
||||
view_proj = self.camera.view_proj_matrix
|
||||
|
||||
m3d.update_audio_system(self.listener, self.sources)
|
||||
|
||||
if (
|
||||
self.water.is_in_fov(cam_pos, cam_dir, self.camera.fov)
|
||||
and np.linalg.norm(self.water.position - cam_pos) < 150
|
||||
):
|
||||
self.water.update_water()
|
||||
self.water.shader.use()
|
||||
self.camera.set_uniforms(self.water.shader)
|
||||
self.water.draw(
|
||||
cam_pos=cam_pos,
|
||||
cam_dir=cam_dir,
|
||||
fov_deg=self.camera.fov,
|
||||
view_proj_matrix=view_proj
|
||||
)
|
||||
|
||||
# Rysuj inne meshe
|
||||
for mesh in self.meshes:
|
||||
if (
|
||||
not mesh.is_in_fov(cam_pos, cam_dir, self.camera.fov)
|
||||
or np.linalg.norm(mesh.position - cam_pos) > 150
|
||||
):
|
||||
continue
|
||||
|
||||
mesh.shader.use()
|
||||
self.camera.set_uniforms(mesh.shader)
|
||||
mesh.draw(
|
||||
cam_pos=cam_pos,
|
||||
cam_dir=cam_dir,
|
||||
fov_deg=self.camera.fov,
|
||||
view_proj_matrix=view_proj,
|
||||
debug=False
|
||||
)
|
||||
|
||||
# FPS tracking
|
||||
self.frames += 1
|
||||
current_time = time.time()
|
||||
if current_time - self.last_fps_time >= 1.0:
|
||||
print(f"FPS: {self.frames}")
|
||||
self.frames = 0
|
||||
self.last_fps_time = current_time
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
m3d.init()
|
||||
self.window.run(self.update)
|
||||
finally:
|
||||
self.water.destroy()
|
||||
for mesh in self.meshes:
|
||||
mesh.destroy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = Game()
|
||||
app.run()
|
||||
73
examples/test.py
Normal file
73
examples/test.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import time
|
||||
import my3dengine as m3d
|
||||
from my3dengine.camera import Camera
|
||||
from OpenGL.GL import *
|
||||
|
||||
class Game:
|
||||
def __init__(self):
|
||||
self.WIDTH, self.HEIGHT = m3d.get_fullscreen()
|
||||
self.window = m3d.Window("3D Game", self.WIDTH, self.HEIGHT)
|
||||
self.keys = m3d.Key
|
||||
self.escape_was_pressed = False
|
||||
|
||||
self.camera = Camera(
|
||||
position=(20, 20, 50),
|
||||
target=(8, 8, 8),
|
||||
up=(0, 1, 0),
|
||||
fov=60,
|
||||
aspect=self.WIDTH / self.HEIGHT,
|
||||
)
|
||||
|
||||
self.base = m3d.Mesh.from_obj("assets/untitled.obj")
|
||||
self.base.set_texture("assets/bricksx64.png")
|
||||
|
||||
spacing = 1.1
|
||||
self.positions = []
|
||||
for x in range(4):
|
||||
for y in range(4):
|
||||
for z in range(4):
|
||||
self.positions.append((x * spacing, y * spacing, z * spacing))
|
||||
|
||||
# FPS counter
|
||||
self.last_fps_time = time.time()
|
||||
self.frames = 0
|
||||
|
||||
def update(self, dt, window):
|
||||
self.camera.handle_input(dt, window)
|
||||
|
||||
glViewport(0, 0, self.WIDTH, self.HEIGHT)
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
view = self.camera.view_matrix
|
||||
proj = self.camera.projection_matrix
|
||||
view_proj = proj @ view
|
||||
|
||||
# Rysujemy tę samą siatkę w wielu pozycjach
|
||||
for pos in self.positions:
|
||||
self.base.set_position(*pos)
|
||||
self.base.draw(
|
||||
cam_pos=self.camera.position,
|
||||
cam_dir=m3d.normalize(self.camera.target - self.camera.position),
|
||||
fov_deg=self.camera.fov,
|
||||
view_proj_matrix=view_proj,
|
||||
debug=False
|
||||
)
|
||||
|
||||
# FPS tracking
|
||||
self.frames += 1
|
||||
current_time = time.time()
|
||||
if current_time - self.last_fps_time >= 1.0:
|
||||
print(f"FPS: {self.frames}")
|
||||
self.frames = 0
|
||||
self.last_fps_time = current_time
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
m3d.init()
|
||||
self.window.run(self.update)
|
||||
finally:
|
||||
self.base.destroy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = Game()
|
||||
app.run()
|
||||
BIN
my3dengine/SDL2.dll
Normal file
BIN
my3dengine/SDL2.dll
Normal file
Binary file not shown.
63
my3dengine/UI.py
Normal file
63
my3dengine/UI.py
Normal 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
15
my3dengine/__init__.py
Normal 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
174
my3dengine/audio.py
Normal 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
113
my3dengine/camera.py
Normal 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
151
my3dengine/core.py
Normal 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
77
my3dengine/key.py
Normal 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
489
my3dengine/mesh.py
Normal 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
131
my3dengine/shader.py
Normal 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
84
my3dengine/water.py
Normal 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])
|
||||
23
setup.py
Normal file
23
setup.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="twoja-nazwa-pakietu",
|
||||
version="0.1.0",
|
||||
author="Twoje Imię",
|
||||
author_email="twoj.email@example.com",
|
||||
description="Krótki opis twojej biblioteki",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
packages=find_packages(),
|
||||
package_data={
|
||||
'my3dengine': ['sdl2.dll'],
|
||||
},
|
||||
include_package_data=True,
|
||||
python_requires=">=3.6",
|
||||
install_requires=[],
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
],
|
||||
)
|
||||
Reference in New Issue
Block a user