1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
|
"""
Wrapper for a loaded scene with properties.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, Optional
import glm
import moderngl
import moderngl_window as mglw
from moderngl_window import geometry
from moderngl_window.meta import ProgramDescription
from moderngl_window.resources.programs import programs
from .material import Material
from .node import Node
from .programs import (
ColorLightProgram,
FallbackProgram,
MeshProgram,
TextureLightProgram,
TextureProgram,
TextureVertexColorProgram,
VertexColorProgram,
)
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from moderngl_window.scene import Camera, Material, Mesh, Node
class Scene:
"""Generic scene"""
def __init__(self, name: Optional[str], **kwargs: Any):
"""Create a scene with a name.
Args:
name (str): Unique name or path for the scene
"""
self.name = name
self.root_nodes: list[Node] = []
# References resources in the scene
self.nodes: list[Node] = []
self.materials: list[Material] = []
self.meshes: list[Mesh] = []
self.cameras: list[Camera] = []
self.bbox_min: glm.vec3 = glm.vec3()
self.bbox_max: glm.vec3 = glm.vec3()
self.diagonal_size = 1.0
self.bbox_vao = geometry.bbox()
if self.ctx.extra is None:
self.ctx.extra = {}
# Load bbox program and cache in the context
self.bbox_program = self.ctx.extra.get("DEFAULT_BBOX_PROGRAM")
if not self.bbox_program:
self.bbox_program = programs.load(
ProgramDescription(path="scene_default/bbox.glsl"),
)
self.ctx.extra["DEFAULT_BBOX_PROGRAM"] = self.bbox_program
# Load wireframe program and cache in the context
self.wireframe_program = self.ctx.extra.get("DEFAULT_WIREFRAME_PROGRAM")
if not self.wireframe_program:
self.wireframe_program = programs.load(
ProgramDescription(path="scene_default/wireframe.glsl"),
)
self.ctx.extra["DEFAULT_WIREFRAME_PROGRAM"] = self.wireframe_program
self._matrix = glm.mat4()
@property
def ctx(self) -> moderngl.Context:
"""moderngl.Context: The current context"""
return mglw.ctx()
@property
def matrix(self) -> glm.mat4:
"""glm.mat4x4: The current model matrix
This property is settable.
"""
return self._matrix
@matrix.setter
def matrix(self, matrix: glm.mat4) -> None:
self._matrix = matrix
for node in self.root_nodes:
node.calc_model_mat(self._matrix)
def draw(
self,
projection_matrix: Optional[glm.mat4],
camera_matrix: Optional[glm.mat4],
time: float = 0.0,
) -> None:
"""Draw all the nodes in the scene.
Args:
projection_matrix (ndarray): projection matrix (bytes)
camera_matrix (ndarray): camera_matrix (bytes)
time (float): The current time
"""
for node in self.root_nodes:
node.draw(
projection_matrix=projection_matrix,
camera_matrix=camera_matrix,
time=time,
)
self.ctx.clear_samplers(0, 4)
def draw_bbox(
self,
projection_matrix: Optional[glm.mat4] = None,
camera_matrix: Optional[glm.mat4] = None,
children: float = True,
color: tuple[float, float, float] = (0.75, 0.75, 0.75),
) -> None:
"""Draw scene and mesh bounding boxes.
Args:
projection_matrix (glm.mat4): mat4 projection
camera_matrix (glm.mat4): mat4 camera matrix
children (bool): Will draw bounding boxes for meshes as well
color (tuple): Color of the bounding boxes
"""
projection_matrix = projection_matrix
camera_matrix = camera_matrix
# Scene bounding box
self.bbox_program["m_proj"].write(projection_matrix)
self.bbox_program["m_model"].write(self._matrix)
self.bbox_program["m_cam"].write(camera_matrix)
self.bbox_program["bb_min"].write(self.bbox_min)
self.bbox_program["bb_max"].write(self.bbox_max)
self.bbox_program["color"].value = color
self.bbox_vao.render(self.bbox_program)
if not children:
return
# Draw bounding box for children
for node in self.root_nodes:
node.draw_bbox(projection_matrix, camera_matrix, self.bbox_program, self.bbox_vao)
def draw_wireframe(
self,
projection_matrix: Optional[glm.mat4] = None,
camera_matrix: Optional[glm.mat4] = None,
color: tuple[float, float, float, float] = (0.75, 0.75, 0.75, 1.0),
) -> None:
"""Render the scene in wireframe mode.
Args:
projection_matrix (ndarray): mat4 projection
camera_matrix (ndarray): mat4 camera matrix
children (bool): Will draw bounding boxes for meshes as well
color (tuple): Color of the wireframes
"""
projection_matrix = projection_matrix
camera_matrix = camera_matrix
self.wireframe_program["m_proj"].write(projection_matrix)
self.wireframe_program["m_model"].write(self._matrix)
self.wireframe_program["m_cam"].write(camera_matrix)
self.wireframe_program["color"] = color
# Draw bounding box for children
self.ctx.wireframe = True
for node in self.root_nodes:
node.draw_wireframe(projection_matrix, camera_matrix, self.wireframe_program)
self.ctx.wireframe = False
def apply_mesh_programs(
self, mesh_programs: Optional[list[MeshProgram]] = None, clear: bool = True
) -> None:
"""Applies mesh programs to meshes.
If not mesh programs are passed in we assign default ones.
Args:
mesh_programs (list): List of mesh programs to assign
clear (bool): Clear all assigned mesh programs
"""
global DEFAULT_PROGRAMS
if clear:
for mesh in self.meshes:
mesh.mesh_program = None
if not mesh_programs:
mesh_programs = self.ctx.extra.get("DEFAULT_PROGRAMS")
if not mesh_programs:
mesh_programs = [
TextureLightProgram(),
TextureProgram(),
VertexColorProgram(),
TextureVertexColorProgram(),
ColorLightProgram(),
FallbackProgram(),
]
self.ctx.extra["DEFAULT_PROGRAMS"] = mesh_programs
for mesh in self.meshes:
for mesh_prog in mesh_programs:
instance = mesh_prog.apply(mesh)
if instance is not None:
if isinstance(instance, MeshProgram):
mesh.mesh_program = mesh_prog
break
else:
raise ValueError(
"apply() must return a MeshProgram instance, not {}".format(
type(instance)
)
)
if not mesh.mesh_program:
logger.warning("WARING: No mesh program applied to '%s'", mesh.name)
def calc_scene_bbox(self) -> None:
"""Calculate scene bbox"""
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)
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.
Calls ``apply_mesh_programs()`` assigning default meshprograms if needed
and sets the model matrix.
"""
self.apply_mesh_programs()
# Recursively calculate model matrices
self.matrix = glm.mat4()
def find_node(self, name: Optional[str] = None) -> Optional[Node]:
"""Finds a :py:class:`~moderngl_window.scene.Node`
Keyword Args:
name (str): Case sensitive name
Returns:
A :py:class:`~moderngl_window.scene.Node` or ``None`` if not found.
"""
for node in self.nodes:
if node.name == name:
return node
return None
def find_material(self, name: Optional[str] = None) -> Optional[Material]:
"""Finds a :py:class:`~moderngl_window.scene.Material`
Keyword Args:
name (str): Case sensitive material name
Returns:
A :py:class:`~moderngl_window.scene.Material` or ``None``
"""
for mat in self.materials:
if mat.name == name:
return mat
return None
def release(self) -> None:
"""Destroys the scene data and vertex buffers"""
self.destroy()
def destroy(self) -> None:
"""Destroys the scene data and vertex buffers"""
for mesh in self.meshes:
if mesh.vao is not None:
mesh.vao.release()
# if mesh.mesh_program:
# mesh.mesh_program.program.release()
for mat in self.materials:
mat.release()
self.meshes = []
self.root_nodes = []
def __str__(self) -> str:
return "<Scene: {}>".format(self.name)
def __repr__(self) -> str:
return str(self)
|