diff --git a/.gitignore b/.gitignore index b91116a..4aea8d0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,33 +8,20 @@ __pycache__/ *.dll # Distribution / packaging -.Python build/ -develop-eggs/ dist/ -downloads/ -eggs/ -.eggs/ lib/ lib64/ -parts/ -sdist/ -var/ -wheels/ *.egg-info/ -.installed.cfg *.egg # Unit test / coverage reports htmlcov/ -.tox/ .coverage .coverage.* .cache -nosetests.xml coverage.xml *.cover -.hypothesis/ .pytest_cache/ # Sphinx documentation @@ -43,26 +30,12 @@ docs/_build/ # IDEs .vscode .idea/ -.spyproject/ # Environments -.env -.venv -.venv35 -.venv36 -.venv37 -.venv38 -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ +.venv* # Misc local -/test.py .mypy_cache -/random_fun/ imgui.ini .DS_Store -/temp/ -video_rec.py \ No newline at end of file +/tmp/ diff --git a/examples/gltf_scenes.py b/examples/gltf_scenes.py index ccd38b9..36a852f 100644 --- a/examples/gltf_scenes.py +++ b/examples/gltf_scenes.py @@ -4,11 +4,10 @@ import moderngl from base import CameraWindow -import moderngl_window as mglw from moderngl_window.scene.camera import KeyboardCamera -class CubeModel(CameraWindow): +class GLTFTest(CameraWindow): """ In oder for this example to work you need to clone the gltf model samples repository and ensure resource_dir is set correctly: @@ -19,17 +18,22 @@ class CubeModel(CameraWindow): window_size = 1280, 720 aspect_ratio = None resource_dir = Path(__file__, "../../../glTF-Sample-Models/2.0").resolve() + # resource_dir = Path(__file__, "../../tmp/issue_with_draco").resolve() def __init__(self, **kwargs): super().__init__(**kwargs) self.wnd.mouse_exclusivity = True + # self.scene = self.load_scene( + # "0147_858530.6583696686_-5537397.529199226_3036197.2162786936.glb", kind="gltf" + # ) + # --- glTF-Sample-Models --- # self.scene = self.load_scene("2CylinderEngine/glTF-Binary/2CylinderEngine.glb") # self.scene = self.load_scene("CesiumMilkTruck/glTF-Embedded/CesiumMilkTruck.gltf") # self.scene = self.load_scene("CesiumMilkTruck/glTF-Binary/CesiumMilkTruck.glb") # self.scene = self.load_scene("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf") - self.scene = self.load_scene("Sponza/glTF/Sponza.gltf") + # self.scene = self.load_scene("Sponza/glTF/Sponza.gltf") # self.scene = self.load_scene("Lantern/glTF-Binary/Lantern.glb") # self.scene = self.load_scene("Buggy/glTF-Binary/Buggy.glb") # self.scene = self.load_scene("VC/glTF-Binary/VC.glb") @@ -61,6 +65,10 @@ def __init__(self, **kwargs): # self.scene = self.load_scene("VertexColorTest/glTF/VertexColorTest.gltf") # self.scene = self.load_scene("WaterBottle/glTF/WaterBottle.gltf") + # --- Draco compressed --- + # self.scene = self.load_scene("Box/glTF-Draco/Box.gltf") + self.scene = self.load_scene("Buggy/glTF-Draco/Buggy.gltf") + self.camera = KeyboardCamera( self.wnd.keys, fov=75.0, @@ -75,34 +83,35 @@ def __init__(self, **kwargs): # if self.scene.diagonal_size > 0: # self.camera.velocity = self.scene.diagonal_size / 5.0 + self.camera.position = ( + self.scene.get_center() + + glm.vec3(0.0, 0.0, self.scene.diagonal_size / 2.0) + ) + def on_render(self, time: float, frame_time: float): """Render the scene""" self.ctx.enable_only(moderngl.DEPTH_TEST | moderngl.CULL_FACE) - # Move camera in on the z axis slightly by default - translation = glm.translate(glm.vec3(0, 0, -1.5)) - camera_matrix = self.camera.matrix * translation - self.scene.draw( projection_matrix=self.camera.projection.matrix, - camera_matrix=camera_matrix, + camera_matrix=self.camera.matrix, time=time, ) - # Draw bounding boxes + # # Draw bounding boxes # self.scene.draw_bbox( # projection_matrix=self.camera.projection.matrix, - # camera_matrix=camera_matrix, + # camera_matrix=self.camera.matrix, # children=True, # color=(0.75, 0.75, 0.75), # ) # self.scene.draw_wireframe( # projection_matrix=self.camera.projection.matrix, - # camera_matrix=camera_matrix, + # camera_matrix=self.camera.matrix, # color=(1, 1, 1, 1), # ) if __name__ == "__main__": - mglw.run_window_config(CubeModel) + GLTFTest.run() diff --git a/moderngl_window/loaders/scene/gltf2.py b/moderngl_window/loaders/scene/gltf2.py index 7e0d70c..f227a53 100644 --- a/moderngl_window/loaders/scene/gltf2.py +++ b/moderngl_window/loaders/scene/gltf2.py @@ -78,7 +78,10 @@ class Loader(BaseLoader): ] #: Supported GLTF extensions #: https://github.com/KhronosGroup/glTF/tree/master/extensions - supported_extensions: list[str] = [] + supported_extensions: list[str] = [ + "KHR_draco_mesh_compression", + "KHR_materials_unlit", + ] meta: SceneDescription @@ -427,15 +430,13 @@ def __init__(self, data: dict[str, Any]): class GLTFMesh: class Primitives: - mode: int | None - accessor: GLTFAccessor | None - def __init__(self, data: dict[str, Any]): self.attributes: dict[str, Any] = data.get("attributes") - self.indices = data.get("indices") - self.mode = data.get("mode") + self.indices: int = data.get("indices") + self.mode: int = data.get("mode") self.material = data.get("material") - self.accessor = None + self.extensions = data.get("extensions", {}) + self.accessor: GLTFAccessor def __init__(self, data: dict[str, Any], meta: SceneDescription): self.meta = meta @@ -453,57 +454,120 @@ def load(self, materials: list[Material]) -> list[Mesh]: "COLOR_0": self.meta.attr_names.COLOR_0, } - meshes = [] + ctx = moderngl_window.ctx() + meshes: list[Mesh] = [] # Read all primitives as separate meshes for now # According to the spec they can have different materials and vertex format for primitive in self.primitives: - vao = VAO(self.name, mode=primitive.mode or moderngl.TRIANGLES) - - # Index buffer - component_type, index_vbo = self.load_indices(primitive) - if index_vbo is not None: - vao.index_buffer( - moderngl_window.ctx().buffer(index_vbo.tobytes()), - index_element_size=component_type.size, - ) + # Handle draco compressed meshes + if primitive.extensions.get("KHR_draco_mesh_compression"): + data = primitive.accessor.read_raw() + import DracoPy + mesh = DracoPy.decode(data) + + attributes = { + 'POSITION': { + 'name': name_map["POSITION"], + 'components': 3, + 'type': 5126, + } + } - attributes = {} - vbos = self.prepare_attrib_mapping(primitive) - - for vbo_info in vbos: - dtype, buffer = vbo_info.create() - vao.buffer( - buffer, - " ".join( - [ - "{}{}".format(attr[1], DTYPE_BUFFER_TYPE[dtype]) - for attr in vbo_info.attributes - ] - ), - [name_map[attr[0]] for attr in vbo_info.attributes], - ) + vao = VAO(self.name, mode=primitive.mode or moderngl.TRIANGLES) + if mesh.faces is not None and mesh.faces.any(): + vao.index_buffer(ctx.buffer(mesh.faces.astype("i4"))) + + vao.buffer(mesh.points.astype("f4"), "3f", name_map["POSITION"]) - for attr in vbo_info.attributes: - attributes[attr[0]] = { - "name": name_map[attr[0]], - "components": attr[1], - "type": vbo_info.component_type.value, + if mesh.tex_coord is not None and mesh.tex_coord.any(): + vao.buffer( + ctx.buffer(mesh.tex_coord.astype("f4")), + "2f", + name_map["TEXCOORD_0"], + ) + attributes["TEXCOORD_0"] = { + 'name': name_map["TEXCOORD_0"], + 'components': 2, + 'type': 5126, + } + if mesh.normals is not None and mesh.normals.any(): + vao.buffer(ctx.buffer(mesh.normals.astype("f4")), "3f", name_map["NORMAL"]) + attributes["NORMAL"] = { + 'name': name_map["NORMAL"], + 'components': 3, + 'type': 5126, + } + if mesh.colors is not None and mesh.colors.any(): + vao.buffer(ctx.buffer(mesh.colors.astype("f4")), "4f", name_map["COLOR_0"]) + attributes["COLOR_0"] = { + 'name': name_map["COLOR_0"], + 'components': 4, + 'type': 5126, } - bbox_min, bbox_max = self.get_bbox(primitive) - meshes.append( - Mesh( - self.name, - vao=vao, - attributes=attributes, - material=( - materials[primitive.material] if primitive.material is not None else None - ), - bbox_min=bbox_min, - bbox_max=bbox_max, + bbox_min, bbox_max = self.get_bbox(primitive) + meshes.append( + Mesh( + self.name, + vao=vao, + attributes=attributes, + material=( + materials[primitive.material] + if primitive.material is not None else None + ), + bbox_min=bbox_min, + bbox_max=bbox_max, + ) + ) + else: + vao = VAO(self.name, mode=primitive.mode or moderngl.TRIANGLES) + + # Index buffer + component_type, index_vbo = self.load_indices(primitive) + if index_vbo is not None: + vao.index_buffer( + ctx.buffer(index_vbo.tobytes()), + index_element_size=component_type.size, + ) + + attributes = {} + vbos = self.prepare_attrib_mapping(primitive) + + for vbo_info in vbos: + dtype, buffer = vbo_info.create() + vao.buffer( + buffer, + " ".join( + [ + "{}{}".format(attr[1], DTYPE_BUFFER_TYPE[dtype]) + for attr in vbo_info.attributes + ] + ), + [name_map[attr[0]] for attr in vbo_info.attributes], + ) + + for attr in vbo_info.attributes: + attributes[attr[0]] = { + "name": name_map[attr[0]], + "components": attr[1], + "type": vbo_info.component_type.value, + } + + bbox_min, bbox_max = self.get_bbox(primitive) + meshes.append( + Mesh( + self.name, + vao=vao, + attributes=attributes, + material=( + materials[primitive.material] + if primitive.material is not None else None + ), + bbox_min=bbox_min, + bbox_max=bbox_max, + ) ) - ) return meshes @@ -621,7 +685,9 @@ def __init__(self, accessor_id: int, data: dict[str, Any]): def read(self) -> tuple[int, ComponentType, npt.NDArray[Any]]: """ Reads buffer data - :return: component count, component type, data + + Return: + component count, component type, data """ # ComponentType helps us determine the datatype dtype = NP_COMPONENT_DTYPE[self.componentType.value] @@ -635,10 +701,19 @@ def read(self) -> tuple[int, ComponentType, npt.NDArray[Any]]: ), ) + def read_raw(self) -> bytes: + """ + Read the raw bytes. Useful for draco compressed meshes or any data that + is not a simple vertex buffer. + """ + return self.bufferView.read_raw() + def info(self) -> tuple[GLTFBuffer, GLTFBufferView, int, int, ComponentType, int, int]: """ Get underlying buffer info for this accessor - :return: buffer, byte_length, byte_offset, component_type, count + + Return: + buffer, byte_length, byte_offset, component_type, count """ buffer, byte_length, byte_offset = self.bufferView.info(byte_offset=self.byteOffset) return ( @@ -743,11 +818,8 @@ def __init__(self, data: dict[str, Any]) -> None: self.rotation = data.get("rotation") self.scale = data.get("scale") - trans_mat = ( - glm.translate(glm.vec3(*self.translation)) - if self.translation is not None - else glm.mat4() - ) + if self.translation is not None: + self.matrix = glm.translate(self.matrix, glm.vec3(*self.translation)) if self.rotation is not None: quat = glm.quat( @@ -756,13 +828,10 @@ def __init__(self, data: dict[str, Any]) -> None: z=self.rotation[2], w=self.rotation[3], ) - rot_mat = glm.mat4_cast(quat) - else: - rot_mat = glm.mat4() - - scale_mat = glm.scale(self.scale) if self.scale is not None else glm.mat4() + self.matrix = self.matrix * glm.mat4_cast(quat) - self.matrix = self.matrix * trans_mat * rot_mat * scale_mat + if self.scale is not None: + self.matrix = glm.scale(self.matrix, glm.vec3(*self.scale)) @property def has_children(self) -> bool: diff --git a/moderngl_window/scene/mesh.py b/moderngl_window/scene/mesh.py index 9915e53..db8e3a5 100644 --- a/moderngl_window/scene/mesh.py +++ b/moderngl_window/scene/mesh.py @@ -51,9 +51,9 @@ def __init__( def draw( self, - projection_matrix: Optional[glm.mat4] = None, - model_matrix: Optional[glm.mat4] = None, - camera_matrix: Optional[glm.mat4] = None, + projection_matrix: glm.mat4, + model_matrix: glm.mat4, + camera_matrix: glm.mat4, time: float = 0.0, ) -> None: """Draw the mesh using the assigned mesh program @@ -64,11 +64,6 @@ def draw( camera_matrix (bytes): camera_matrix """ if self.mesh_program is not None: - assert ( - projection_matrix is not None - ), "Can not draw, there is no projection matrix to use" - assert model_matrix is not None, "Can not draw, there is no model matrix to use" - assert camera_matrix is not None, "Can not draw, there is no camera matrix to use" self.mesh_program.draw( self, projection_matrix=projection_matrix, @@ -125,7 +120,7 @@ def add_attribute(self, attr_type: str, name: str, components: int) -> None: self.attributes[attr_type] = {"name": name, "components": components} def calc_global_bbox( - self, view_matrix: glm.mat4, bbox_min: Optional[glm.vec3], bbox_max: Optional[glm.vec3] + self, view_matrix: glm.mat4, bbox_min: glm.vec3 | None, bbox_max: glm.vec3 | None ) -> tuple[glm.vec3, glm.vec3]: """Calculates the global bounding. diff --git a/moderngl_window/scene/node.py b/moderngl_window/scene/node.py index 673ad8f..68aa696 100644 --- a/moderngl_window/scene/node.py +++ b/moderngl_window/scene/node.py @@ -104,8 +104,8 @@ def add_child(self, node: "Node") -> None: def draw( self, - projection_matrix: Optional[glm.mat4], - camera_matrix: Optional[glm.mat4], + projection_matrix: glm.mat4, + camera_matrix: glm.mat4, time: float = 0.0, ) -> None: """Draw node and children. @@ -182,7 +182,7 @@ def draw_wireframe( child.draw_wireframe(projection_matrix, self._matrix_global, program) def calc_global_bbox( - self, view_matrix: glm.mat4, bbox_min: Optional[glm.vec3], bbox_max: Optional[glm.vec3] + self, view_matrix: glm.mat4, bbox_min: glm.vec3 | None, bbox_max: glm.vec3 | None ) -> tuple[glm.vec3, glm.vec3]: """Recursive calculation of scene bbox. diff --git a/moderngl_window/scene/scene.py b/moderngl_window/scene/scene.py index 37126be..cd182f4 100644 --- a/moderngl_window/scene/scene.py +++ b/moderngl_window/scene/scene.py @@ -97,8 +97,8 @@ def matrix(self, matrix: glm.mat4) -> None: def draw( self, - projection_matrix: Optional[glm.mat4] = None, - camera_matrix: Optional[glm.mat4] = None, + projection_matrix: Optional[glm.mat4], + camera_matrix: Optional[glm.mat4], time: float = 0.0, ) -> None: """Draw all the nodes in the scene. @@ -229,20 +229,21 @@ def apply_mesh_programs( def calc_scene_bbox(self) -> None: """Calculate scene bbox""" - bbox_min: Optional[glm.vec3] = None - bbox_max: Optional[glm.vec3] = None + bbox_min: glm.vec3 | None = None + bbox_max: glm.vec3 | None = None + for node in self.root_nodes: bbox_min, bbox_max = node.calc_global_bbox(glm.mat4(), bbox_min, bbox_max) - assert (bbox_max is not None) and ( - bbox_min is not None - ), "The bounding are not defined, please make sure your code is correct" - self.bbox_min = bbox_min self.bbox_max = bbox_max self.diagonal_size = glm.length(self.bbox_max - self.bbox_min) + def get_center(self) -> glm.vec3: + """Calculate the center of the scene using bounding boxes""" + return self.bbox_min + (self.bbox_max - self.bbox_min) / 2.0 + def prepare(self) -> None: """prepare the scene for rendering.