Lagrange
bind_scene.h
1/*
2 * Copyright 2023 Adobe. All rights reserved.
3 * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License. You may obtain a copy
5 * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 *
7 * Unless required by applicable law or agreed to in writing, software distributed under
8 * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 * OF ANY KIND, either express or implied. See the License for the specific language
10 * governing permissions and limitations under the License.
11 */
12#pragma once
13
14#include <lagrange/AttributeValueType.h>
15#include <lagrange/Logger.h>
16#include <lagrange/python/tensor_utils.h>
17#include <lagrange/scene/Scene.h>
18#include <lagrange/scene/scene_utils.h>
19#include <lagrange/utils/assert.h>
20
21#include "bind_value.h"
22
23// clang-format off
24#include <lagrange/utils/warnoff.h>
25#include <Eigen/Core>
26#include <nanobind/nanobind.h>
27#include <nanobind/eigen/dense.h>
28#include <nanobind/stl/string.h>
29#include <nanobind/stl/filesystem.h>
30#include <nanobind/stl/array.h>
31#include <nanobind/stl/variant.h>
32#include <nanobind/stl/optional.h>
33#include <nanobind/stl/bind_vector.h>
34#include <nanobind/stl/bind_map.h>
35#include <nanobind/trampoline.h>
36#include <lagrange/utils/warnon.h>
37// clang-format on
38
39
40namespace lagrange::python {
41
42namespace nb = nanobind;
43
44#pragma GCC visibility push(hidden)
46{
47 NB_TRAMPOLINE(lagrange::scene::UserDataConverter, 5);
48
49 bool is_supported(const std::string& key) const override
50 {
51 NB_OVERRIDE_PURE(is_supported, key);
52 }
53 bool can_read(const std::string& key) const override { NB_OVERRIDE(can_read, key); }
54 bool can_write(const std::string& key) const override { NB_OVERRIDE(can_write, key); }
55 std::any read(const lagrange::scene::Value& value) const override { NB_OVERRIDE(read, value); }
56 lagrange::scene::Value write(const std::any& value) const override
57 {
58 NB_OVERRIDE(write, value);
59 }
60};
61#pragma GCC visibility pop
62
63void bind_scene(nb::module_& m)
64{
65 using namespace lagrange::scene;
66 using Scalar = double;
67 using Index = uint32_t;
68 using SceneType = Scene<Scalar, Index>;
69
70 nb::bind_vector<std::vector<Node>>(m, "NodeList");
71 nb::bind_vector<std::vector<ElementId>>(m, "ElementIdList");
72 nb::bind_vector<std::vector<SceneMeshInstance>>(m, "SceneMeshInstanceList");
73 nb::bind_vector<std::vector<SurfaceMesh<Scalar, Index>>>(m, "SurfaceMeshList");
74 nb::bind_vector<std::vector<ImageExperimental>>(m, "ImageList");
75 nb::bind_vector<std::vector<Texture>>(m, "TextureList");
76 nb::bind_vector<std::vector<MaterialExperimental>>(m, "MaterialList");
77 nb::bind_vector<std::vector<Light>>(m, "LightList");
78 nb::bind_vector<std::vector<Camera>>(m, "CameraList");
79 nb::bind_vector<std::vector<Skeleton>>(m, "SkeletonList");
80 nb::bind_vector<std::vector<Animation>>(m, "AnimationList");
81
82
83 nb::bind_vector<std::vector<lagrange::scene::Value>>(m, "ValueList");
84 nb::bind_vector<std::vector<unsigned char>>(m, "BufferList");
85 nb::bind_map<std::unordered_map<std::string, lagrange::scene::Value>>(m, "ValueUnorderedMap");
86 nb::bind_map<std::map<std::string, lagrange::scene::Value>>(m, "ValueMap");
87 // nb::bind_map<std::unordered_map<std::string, std::any>>(m, "anyMap");
88
89
90 nb::class_<lagrange::scene::Extensions>(m, "Extensions")
91 .def_prop_ro("size", &Extensions::size)
92 .def_prop_ro("empty", &Extensions::empty)
93 .def_rw("data", &Extensions::data);
94 // .def_prop_rw(
95 // "user_data",
96 // [](const Extensions& ext) -> std::unordered_map<std::string, void*> {
97 // std::unordered_map<std::string, void*> ret;
98 // for (const auto& [key, val] : ext.user_data) {
99 // ret.insert({key, std::any_cast<void*>(val)});
100 // }
101 // return ret;
102 // },
103 // [](Extensions& ext, std::unordered_map<std::string, void*> data) -> void {
104 // ext.user_data.clear();
105 // for (const auto& [key, val] : data) {
106 // ext.user_data.insert({key, val});
107 // }
108 // });
109
110 // nb::class_<UserDataConverter, UserDataConverterTrampoline>(m, "UserExtension")
111 // .def("is_supported", &UserDataConverter::is_supported)
112 // .def("can_read", &UserDataConverter::can_read)
113 // .def("can_write", &UserDataConverter::can_write)
114 // .def("read", &UserDataConverter::read)
115 // .def("write", &UserDataConverter::write);
116
117 nb::class_<SceneMeshInstance>(m, "SceneMeshInstance", "Mesh and material index of a node")
118 .def(nb::init<>())
119 .def_rw("mesh", &SceneMeshInstance::mesh)
120 .def_rw("materials", &SceneMeshInstance::materials);
121
122 nb::class_<Node>(m, "Node")
123 .def(nb::init<>())
124 .def("__repr__", [](const Node& node) { return fmt::format("Node('{}')", node.name); })
125 .def_rw("name", &Node::name)
126 .def_prop_rw(
127 "transform",
128 [](Node& node) {
129 return nb::ndarray<nb::numpy, float, nb::f_contig, nb::shape<4, 4>>(
130 node.transform.data(),
131 {4, 4},
132 nb::find(node),
133 {1, 4});
134 },
135 [](Node& node, nb::ndarray<nb::numpy, const float, nb::shape<4, 4>> t) -> void {
136 auto view = t.view<float, nb::ndim<2>>();
137 // Explicit 2D indexing because the input ndarray can be either row or column major.
138 for (size_t i = 0; i < 4; i++) {
139 for (size_t j = 0; j < 4; j++) {
140 node.transform.data()[i + j * 4] = view(i, j);
141 }
142 }
143 },
144 "The affine transform associated with this node")
145 .def_rw("parent", &Node::parent)
146 .def_rw("children", &Node::children)
147 .def_rw("meshes", &Node::meshes)
148 .def_rw("cameras", &Node::cameras)
149 .def_rw("lights", &Node::lights)
150 .def_rw("extensions", &Node::extensions);
151
152 nb::class_<ImageBufferExperimental> image_buffer(m, "ImageBuffer");
153 image_buffer.def(nb::init<>())
154 .def_ro("width", &ImageBufferExperimental::width, "Image width")
155 .def_ro("height", &ImageBufferExperimental::height, "Image height")
156 .def_ro(
157 "num_channels",
158 &ImageBufferExperimental::num_channels,
159 "Number of channels in each pixel")
160 .def_prop_rw(
161 "data",
162 [](ImageBufferExperimental& self) {
163 size_t shape[3] = {self.height, self.width, self.num_channels};
164 switch (self.element_type) {
165 case AttributeValueType::e_int8_t:
166 return nb::cast(
167 nb::ndarray<int8_t, nb::numpy, nb::c_contig, nb::device::cpu>(
168 reinterpret_cast<int8_t*>(self.data.data()),
169 3,
170 shape,
171 nb::find(self)),
172 nb::rv_policy::reference_internal);
173 case AttributeValueType::e_uint8_t:
174 return nb::cast(
175 nb::ndarray<uint8_t, nb::numpy, nb::c_contig, nb::device::cpu>(
176 reinterpret_cast<uint8_t*>(self.data.data()),
177 3,
178 shape,
179 nb::find(self)),
180 nb::rv_policy::reference_internal);
181 case AttributeValueType::e_int16_t:
182 return nb::cast(
183 nb::ndarray<int16_t, nb::numpy, nb::c_contig, nb::device::cpu>(
184 reinterpret_cast<int16_t*>(self.data.data()),
185 3,
186 shape,
187 nb::find(self)),
188 nb::rv_policy::reference_internal);
189 case AttributeValueType::e_uint16_t:
190 return nb::cast(
191 nb::ndarray<uint16_t, nb::numpy, nb::c_contig, nb::device::cpu>(
192 reinterpret_cast<uint16_t*>(self.data.data()),
193 3,
194 shape,
195 nb::find(self)),
196 nb::rv_policy::reference_internal);
197 case AttributeValueType::e_int32_t:
198 return nb::cast(
199 nb::ndarray<int32_t, nb::numpy, nb::c_contig, nb::device::cpu>(
200 reinterpret_cast<int32_t*>(self.data.data()),
201 3,
202 shape,
203 nb::find(self)),
204 nb::rv_policy::reference_internal);
205 case AttributeValueType::e_uint32_t:
206 return nb::cast(
207 nb::ndarray<uint32_t, nb::numpy, nb::c_contig, nb::device::cpu>(
208 reinterpret_cast<uint32_t*>(self.data.data()),
209 3,
210 shape,
211 nb::find(self)),
212 nb::rv_policy::reference_internal);
213 case AttributeValueType::e_int64_t:
214 return nb::cast(
215 nb::ndarray<int64_t, nb::numpy, nb::c_contig, nb::device::cpu>(
216 reinterpret_cast<int64_t*>(self.data.data()),
217 3,
218 shape,
219 nb::find(self)),
220 nb::rv_policy::reference_internal);
221 case AttributeValueType::e_uint64_t:
222 return nb::cast(
223 nb::ndarray<uint64_t, nb::numpy, nb::c_contig, nb::device::cpu>(
224 reinterpret_cast<uint64_t*>(self.data.data()),
225 3,
226 shape,
227 nb::find(self)),
228 nb::rv_policy::reference_internal);
229 case AttributeValueType::e_float:
230 return nb::cast(
231 nb::ndarray<float, nb::numpy, nb::c_contig, nb::device::cpu>(
232 reinterpret_cast<float*>(self.data.data()),
233 3,
234 shape,
235 nb::find(self)),
236 nb::rv_policy::reference_internal);
237 case AttributeValueType::e_double:
238 return nb::cast(
239 nb::ndarray<double, nb::numpy, nb::c_contig, nb::device::cpu>(
240 reinterpret_cast<double*>(self.data.data()),
241 3,
242 shape,
243 nb::find(self)),
244 nb::rv_policy::reference_internal);
245 default: throw nb::type_error("Unsupported image buffer `dtype`!");
246 }
247 },
249 nb::ndarray<nb::numpy, nb::c_contig, nb::device::cpu> tensor) {
250 la_runtime_assert(tensor.ndim() == 3);
251 self.width = tensor.shape(1);
252 self.height = tensor.shape(0);
253 self.num_channels = tensor.shape(2);
254 auto dtype = tensor.dtype();
255 if (dtype == nb::dtype<int8_t>()) {
256 self.element_type = AttributeValueType::e_int8_t;
257 } else if (dtype == nb::dtype<uint8_t>()) {
258 self.element_type = AttributeValueType::e_uint8_t;
259 } else if (dtype == nb::dtype<int16_t>()) {
260 self.element_type = AttributeValueType::e_int16_t;
261 } else if (dtype == nb::dtype<uint16_t>()) {
262 self.element_type = AttributeValueType::e_uint16_t;
263 } else if (dtype == nb::dtype<int32_t>()) {
264 self.element_type = AttributeValueType::e_int32_t;
265 } else if (dtype == nb::dtype<uint32_t>()) {
266 self.element_type = AttributeValueType::e_uint32_t;
267 } else if (dtype == nb::dtype<int64_t>()) {
268 self.element_type = AttributeValueType::e_int64_t;
269 } else if (dtype == nb::dtype<uint64_t>()) {
270 self.element_type = AttributeValueType::e_uint64_t;
271 } else if (dtype == nb::dtype<float>()) {
272 self.element_type = AttributeValueType::e_float;
273 } else if (dtype == nb::dtype<double>()) {
274 self.element_type = AttributeValueType::e_double;
275 } else {
276 throw nb::type_error("Unsupported input tensor `dtype`!");
277 }
278 self.data.resize(tensor.nbytes());
279 std::copy(
280 reinterpret_cast<uint8_t*>(tensor.data()),
281 reinterpret_cast<uint8_t*>(tensor.data()) + tensor.nbytes(),
282 self.data.data());
283 },
284 "Raw image data.")
285 .def_prop_ro(
286 "dtype",
287 [](ImageBufferExperimental& self) -> std::optional<nb::type_object> {
288 auto np = nb::module_::import_("numpy");
289 switch (self.element_type) {
290 case AttributeValueType::e_int8_t: return np.attr("int8");
291 case AttributeValueType::e_int16_t: return np.attr("int16");
292 case AttributeValueType::e_int32_t: return np.attr("int32");
293 case AttributeValueType::e_int64_t: return np.attr("int64");
294 case AttributeValueType::e_uint8_t: return np.attr("uint8");
295 case AttributeValueType::e_uint16_t: return np.attr("uint16");
296 case AttributeValueType::e_uint32_t: return np.attr("uint32");
297 case AttributeValueType::e_uint64_t: return np.attr("uint64");
298 case AttributeValueType::e_float: return np.attr("float32");
299 case AttributeValueType::e_double: return np.attr("float64");
300 default: logger().warn("Image buffer has an unknown dtype."); return std::nullopt;
301 }
302 },
303 "The element data type of the image buffer.");
304
305 nb::class_<ImageExperimental> image(m, "Image");
306 image.def(nb::init<>())
307 .def_rw("name", &ImageExperimental::name, "Name of the image object")
308 .def_rw("image", &ImageExperimental::image, "Image buffer")
309 .def_rw("uri", &ImageExperimental::uri, "URI of the image file")
310 .def_rw(
311 "extensions",
312 &ImageExperimental::extensions,
313 "Additional data associated with the image")
314 .def("__repr__", [](const ImageExperimental& self) {
315 return fmt::format("Image ('{}')", self.name);
316 });
317
318 nb::class_<TextureInfo>(m, "TextureInfo")
319 .def(nb::init<>())
320 .def_rw("index", &TextureInfo::index)
321 .def_rw("texcoord", &TextureInfo::texcoord);
322
323 nb::class_<MaterialExperimental> material(m, "Material");
324 material.def(nb::init<>())
325 .def(
326 "__repr__",
327 [](const MaterialExperimental& mat) { return fmt::format("Material('{}')", mat.name); })
328 .def_rw("name", &MaterialExperimental::name)
329 .def_rw("base_color_value", &MaterialExperimental::base_color_value)
330 .def_rw("base_color_texture", &MaterialExperimental::base_color_texture)
331 .def_rw("alpha_mode", &MaterialExperimental::alpha_mode)
332 .def_rw("alpha_cutoff", &MaterialExperimental::alpha_cutoff)
333 .def_rw("emissive_value", &MaterialExperimental::emissive_value)
334 .def_rw("emissive_texture", &MaterialExperimental::emissive_texture)
335 .def_rw("metallic_value", &MaterialExperimental::metallic_value)
336 .def_rw("roughness_value", &MaterialExperimental::roughness_value)
337 .def_rw("metallic_roughness_texture", &MaterialExperimental::metallic_roughness_texture)
338 .def_rw("normal_texture", &MaterialExperimental::normal_texture)
339 .def_rw("normal_scale", &MaterialExperimental::normal_scale)
340 .def_rw("occlusion_texture", &MaterialExperimental::occlusion_texture)
341 .def_rw("occlusion_strength", &MaterialExperimental::occlusion_strength)
342 .def_rw("double_sided", &MaterialExperimental::double_sided)
343 .def_rw("extensions", &MaterialExperimental::extensions);
344
345 nb::enum_<MaterialExperimental::AlphaMode>(material, "AlphaMode")
346 .value("Opaque", MaterialExperimental::AlphaMode::Opaque)
347 .value("Mask", MaterialExperimental::AlphaMode::Mask)
348 .value("Blend", MaterialExperimental::AlphaMode::Blend);
349
350
351 nb::class_<Texture> texture(m, "Texture", "Texture");
352 texture.def(nb::init<>())
353 .def("__repr__", [](const Texture& t) { return fmt::format("Texture('{}')", t.name); })
354 .def_rw("name", &Texture::name)
355 .def_rw("image", &Texture::image)
356 .def_rw("mag_filter", &Texture::mag_filter)
357 .def_rw("min_filter", &Texture::min_filter)
358 .def_rw("wrap_u", &Texture::wrap_u)
359 .def_rw("wrap_v", &Texture::wrap_v)
360 .def_rw("scale", &Texture::scale)
361 .def_rw("offset", &Texture::offset)
362 .def_rw("rotation", &Texture::rotation)
363 .def_rw("extensions", &Texture::extensions);
364
365 nb::enum_<Texture::WrapMode>(texture, "WrapMode")
366 .value("Wrap", Texture::WrapMode::Wrap)
367 .value("Clamp", Texture::WrapMode::Clamp)
368 .value("Decal", Texture::WrapMode::Decal)
369 .value("Mirror", Texture::WrapMode::Mirror);
370 nb::enum_<Texture::TextureFilter>(texture, "TextureFilter")
371 .value("Undefined", Texture::TextureFilter::Undefined)
372 .value("Nearest", Texture::TextureFilter::Nearest)
373 .value("Linear", Texture::TextureFilter::Linear)
374 .value("NearestMimpapNearest", Texture::TextureFilter::NearestMimpapNearest)
375 .value("LinearMipmapNearest", Texture::TextureFilter::LinearMipmapNearest)
376 .value("NearestMipmapLinear", Texture::TextureFilter::NearestMipmapLinear)
377 .value("LinearMipmapLinear", Texture::TextureFilter::LinearMipmapLinear);
378
379 nb::class_<Light> light(m, "Light", "Light");
380 light.def(nb::init<>())
381 .def("__repr__", [](const Light& l) { return fmt::format("Light('{}')", l.name); })
382 .def_rw("name", &Light::name)
383 .def_rw("type", &Light::type)
384 .def_rw("position", &Light::position)
385 .def_rw("direction", &Light::direction)
386 .def_rw("up", &Light::up)
387 .def_rw("intensity", &Light::intensity)
388 .def_rw("attenuation_constant", &Light::attenuation_constant)
389 .def_rw("attenuation_linear", &Light::attenuation_linear)
390 .def_rw("attenuation_quadratic", &Light::attenuation_quadratic)
391 .def_rw("attenuation_cubic", &Light::attenuation_cubic)
392 .def_rw("range", &Light::range)
393 .def_rw("color_diffuse", &Light::color_diffuse)
394 .def_rw("color_specular", &Light::color_specular)
395 .def_rw("color_ambient", &Light::color_ambient)
396 .def_rw("angle_inner_cone", &Light::angle_inner_cone)
397 .def_rw("angle_outer_cone", &Light::angle_outer_cone)
398 .def_rw("size", &Light::size)
399 .def_rw("extensions", &Light::extensions);
400
401 nb::enum_<Light::Type>(light, "Type")
402 .value("Undefined", Light::Type::Undefined)
403 .value("Directional", Light::Type::Directional)
404 .value("Point", Light::Type::Point)
405 .value("Spot", Light::Type::Spot)
406 .value("Ambient", Light::Type::Ambient)
407 .value("Area", Light::Type::Area);
408
409 nb::class_<Camera> camera(m, "Camera", "Camera");
410 camera.def(nb::init<>())
411 .def("__repr__", [](const Camera& c) { return fmt::format("Camera('{}')", c.name); })
412 .def_rw("name", &Camera::name)
413 .def_rw("position", &Camera::position)
414 .def_rw("up", &Camera::up)
415 .def_rw("look_at", &Camera::look_at)
416 .def_rw("near_plane", &Camera::near_plane)
417 .def_rw("far_plane", &Camera::far_plane)
418 .def_rw("type", &Camera::type)
419 .def_rw("aspect_ratio", &Camera::aspect_ratio)
420 .def_rw("horizontal_fov", &Camera::horizontal_fov)
421 .def_rw("orthographic_width", &Camera::orthographic_width)
422 .def_prop_ro("get_vertical_fov", &Camera::get_vertical_fov)
423 .def_prop_ro(
424 "set_horizontal_fov_from_vertical_fov",
425 &Camera::set_horizontal_fov_from_vertical_fov,
426 "vfov"_a)
427 .def_rw("extensions", &Camera::extensions);
428
429 nb::enum_<Camera::Type>(camera, "Type")
430 .value("Perspective", Camera::Type::Perspective)
431 .value("Orthographic", Camera::Type::Orthographic);
432
433 nb::class_<Animation>(m, "Animation", "")
434 .def(nb::init<>())
435 .def("__repr__", [](const Animation& a) { return fmt::format("Animation('{}')", a.name); })
436 .def_rw("name", &Animation::name)
437 .def_rw("extensions", &Animation::extensions);
438
439
440 nb::class_<Skeleton>(m, "Skeleton", "")
441 .def(nb::init<>())
442 .def_rw("meshes", &Skeleton::meshes)
443 .def_rw("extensions", &Skeleton::extensions);
444
445
446 nb::class_<SceneType>(m, "Scene", "A 3D scene")
447 .def(nb::init<>())
448 .def("__repr__", [](const SceneType& s) { return fmt::format("Scene('{}')", s.name); })
449 .def_rw("name", &SceneType::name)
450 .def_rw("nodes", &SceneType::nodes)
451 .def_rw("root_nodes", &SceneType::root_nodes)
452 .def_rw("meshes", &SceneType::meshes)
453 .def_rw("images", &SceneType::images)
454 .def_rw("textures", &SceneType::textures)
455 .def_rw("materials", &SceneType::materials)
456 .def_rw("lights", &SceneType::lights)
457 .def_rw("cameras", &SceneType::cameras)
458 .def_rw("skeletons", &SceneType::skeletons)
459 .def_rw("animations", &SceneType::animations)
460 .def_rw("extensions", &SceneType::extensions)
461 .def(
462 "add",
463 [](SceneType& self,
464 std::variant<
465 Node,
466 SceneType::MeshType,
468 Texture,
470 Light,
471 Camera,
472 Skeleton,
473 Animation> element) {
474 return std::visit(
475 [&](auto&& value) {
476 using T = std::decay_t<decltype(value)>;
477 return self.add(std::forward<T>(value));
478 },
479 element);
480 },
481 "element"_a,
482 R"(Add an element to the scene.
483
484:param element: The element to add to the scene. E.g. node, mesh, image, texture, material, light, camera, skeleton, or animation.
485
486:returns: The id of the added element.)")
487 .def(
488 "add_child",
489 &SceneType::add_child,
490 "parent_id"_a,
491 "child_id"_a,
492 R"(Add a child node to a parent node.
493
494:param parent_id: The parent node id.
495:param child_id: The child node id.
496
497:returns: The id of the added child node.)");
498
499 m.def(
500 "compute_global_node_transform",
501 [](const SceneType& scene, size_t node_idx) {
502 auto t = utils::compute_global_node_transform<Scalar, Index>(scene, node_idx);
503 return nb::ndarray<nb::numpy, float, nb::f_contig, nb::shape<4, 4>>(
504 t.data(),
505 {4, 4},
506 nb::handle(), // owner
507 {1, 4});
508 },
509 nb::rv_policy::copy,
510 "scene"_a,
511 "node_idx"_a,
512 R"(Compute the global transform associated with a node.
513
514:param scene: The input node.
515:param node_idx: The index of the taget node.
516
517:returns: The global transform of the target node, which is the combination of transforms from this node all the way to the root.
518 )");
519}
520
521} // namespace lagrange::python
Definition: SceneExtension.h:33
LA_CORE_API spdlog::logger & logger()
Retrieves the current logger.
Definition: Logger.cpp:40
#define la_runtime_assert(...)
Runtime assertion check.
Definition: assert.h:169
internal::Range< Index > range(Index end)
Returns an iterable object representing the range [0, end).
Definition: range.h:176
Definition: Scene.h:329
Definition: Scene.h:271
Minimalistic image data structure that stores the raw image data.
Definition: Scene.h:87
size_t height
Image height.
Definition: Scene.h:92
size_t width
Image width.
Definition: Scene.h:89
AttributeValueType element_type
The scalar type of the elements in the buffer.
Definition: Scene.h:98
std::vector< unsigned char > data
Raw buffer of size (width * height * num_channels * num_bits_per_element / 8) bytes containing image ...
Definition: Scene.h:101
size_t num_channels
Number of image channels (must be 1, 3, or 4).
Definition: Scene.h:95
Image structure that can store either image data or reference to an image file.
Definition: Scene.h:113
std::string name
Image name. Not guaranteed to be unique and can be empty.
Definition: Scene.h:115
Definition: Scene.h:225
Definition: Scene.h:55
Definition: Scene.h:349
Definition: Scene.h:337
Definition: Scene.h:185
Definition: SceneExtension.h:182