3D Engine on Python (renderer, physics)
Tip
Controls
W/A/S/D - Movement
Ctrl - acceleration
F - toggle flashlight
F5 - take screenshot
Caution
If you want to build project from sources on your PC, please, check requirements.txt
This was a bold attempt to create a full-fledged 3D engine on Python, but Python has its own perfomance limitations.
- Renderer
- VAO Instancing (
EntityCluster) - Simple shading model (point and spot light sources)
- Basic rigidbody
- AABB collider
- FPSController based on AABB collider
It has no physics on demo_level because it created of complex meshes
Demo level has been described in src/levels/test_level.py class.
You can create your own level following next steps
Create new file in levels directory using this pattern:
from src.settings import *
from src.scene import Scene
from src.entity.entity import Entity
from src.texture import TextureManager
from src.mesh.utils import load_from_obj
from src.mesh.single_mesh import SingleMesh
from src.mesh.instanced_mesh import InstancedMesh
from src.entity.entity_cluster import EntityCluster
class MyLevel(Scene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Init your objects
def update(self, time, delta_time):
# Update your objects
super().update(time, delta_time)Tip
Initialization of objects on the scene includes several steps
- Init vertex data
- Init mesh object
- Init texture object
- Init entity object\objects related to this mesh
- Init entity cluster if you want to draw multiple objects using same mesh in one draw call
- Append entity or entity cluster to scene
Put in your obj model in assets/obj directory
Use following example code in MyLevel.__init__
your_obj = load_from_obj('obj/your_obj')
your_obj_mesh = SingleMesh(
ctx=self.ctx, program=self.shader.program,
position = your_obj['position'],
position_indices = your_obj['position_indices'],
normal = your_obj['normal'],
normal_indices = your_obj['normal_indices'],
texcoord = your_obj['texcoord'],
texcoord_indices = your_obj['texcoord_indices'],
decrement=1,
name='your_obj_mesh'
)decrement field is needed to subtract some value while parsing mesh index data. *.obj default start index is 1, but my mesh parser uses 0 start index.
AvecdesEngine does not support the use of *.mtl files.
Tip
One mesh - one texture
You can load the texture as follows
Warning
PNG file format only
TextureManager.load_texture
(
context: mgl.Context,
assets_path: str,
has_alpha: bool,
filter_type: int
)
Example for loading non-alpha texture with closest interpolation type
your_texture = TextureManager.load_texture(self.ctx, 'textures/your_texture', False, mgl.NEAREST)your_entity = Entity(
mesh=your_obj_mesh,
name='your_entity',
texture=your_texture,
collider='none',
)
self.append_object(your_entity)Note
EntityCluster is collection of multiple entities that will be drawn per one draw call.
Entities attached to EntityCluster uses InstancedMesh mesh type.
First of all create InstancedMesh instance the same way like SingleMesh type.
your_model_obj = load_from_obj('obj/your_obj')
your_mesh = InstancedMesh(
ctx = self.ctx, program = self.shader.program,
position = your_model_obj['position'],
position_indices = your_model_obj['position_indices'],
normal = your_model_obj['normal'],
normal_indices = your_model_obj['normal_indices'],
texcoord = your_model_obj['texcoord'],
texcoord_indices = your_model_obj['texcoord_indices'],
decrement=1,
name='your_model_instanced'
)Next, initialize cluster instance and objects.
cluster = EntityCluster('your_cluster', your_ins_mesh, your_texture)For example:
grid_w, grid_d, size = (10, 10, 4)
for i in range(grid_w * grid_d):
x = (i % grid_w) * size
y = (i // grid_d) * size
your_entity = Entity(
pos = (x, 0, y),
name = 'your_entity_name',
mesh = 'root',
collider = 'aabb',
use_physics = True,
use_gravity = True,
texture = 'root',
)
your_entity.collider = AABB(your_entity, 2.0, 1.0, 3.0)
# Add object to cluster
cluster.append_object(your_entity)
# After adding all objects cluster need to be processed
cluster.process()
# Add cluster to scene
self.append_object(cluster)Note
You can dynamically add objects to the cluster after the application has started.
Note
The cluster updates only those objects that have changed (my attempt at optimization)
Add your file to the imports section of the src/engine.py class.
# other imports ...
from src.levels.my_level import MyLevelFind the init_scene function in the Engine class and change the name of TestLevel to the name of your class.
def init_scene(self):
self.scene_light_shader = Shader(self.ctx, 'd_light')
# Change below
self.scene = MyLevel(self, self.scene_light_shader, 'test_level')
self.player = Player(self.wnd, self.scene_light_shader)self.player = FPSController(self, self.scene_light_shader)
self.scene.append_object(self.player)Call update_player method in update method of engine class.
if self.wnd.mouse_exclusivity:
self.player.movement(self.delta_time)
# Change below
self.player.update_player()- ModernGL (simple OpenGL API)
- Pillow Image
- Numpy
- OpenGL Math lib (GLM)
Next, I would like to implement a wider functionality of the 3D engine but already on C++.
- GJK collision algo
- Level loader
- Enemy behaviour
- Instanced mesh based UI


