python/scene.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file scene.cpp | ||
| 3 | /// @brief Python bindings of the scene: prims, attributes as Python values, arrays shared through | ||
| 4 | /// DLPack, JSON scene files and transforms. Values pass as scene files write them, so Python reads | ||
| 5 | /// and authors attributes by the same rules as JSON. | ||
| 6 | #include "ernie/python/scene.h" | ||
| 7 | |||
| 8 | #include "ernie/python/dlpack.h" | ||
| 9 | #include "ernie/scene/json.h" | ||
| 10 | #include "ernie/scene/json_value.h" | ||
| 11 | #include "ernie/scene/scene.h" | ||
| 12 | #include "ernie/scene/schema.h" | ||
| 13 | #include "ernie/scene/tokens.h" | ||
| 14 | #include "ernie/scene/xform.h" | ||
| 15 | #include "ernie/util/algorithm.h" | ||
| 16 | |||
| 17 | #include <nanobind/stl/array.h> | ||
| 18 | #include <nanobind/stl/filesystem.h> | ||
| 19 | #include <nanobind/stl/optional.h> | ||
| 20 | #include <nanobind/stl/string.h> | ||
| 21 | #include <nanobind/stl/string_view.h> | ||
| 22 | #include <nanobind/stl/tuple.h> | ||
| 23 | #include <nanobind/stl/variant.h> | ||
| 24 | #include <nanobind/stl/vector.h> | ||
| 25 | |||
| 26 | #include <cstdint> | ||
| 27 | #include <filesystem> | ||
| 28 | #include <functional> | ||
| 29 | #include <optional> | ||
| 30 | #include <string> | ||
| 31 | #include <string_view> | ||
| 32 | #include <variant> | ||
| 33 | #include <vector> | ||
| 34 | |||
| 35 | namespace nb = nanobind; | ||
| 36 | |||
| 37 | namespace ernie::python { | ||
| 38 | namespace { | ||
| 39 | |||
| 40 | /// A prim of a scene, as long as it is not removed; it keeps its scene alive. | ||
| 41 | 187 | struct Prim { | |
| 42 | PrimId id; | ||
| 43 | nb::object scene; | ||
| 44 | |||
| 45 | 47 | Scene& Owner() const { | |
| 46 | 47 | return nb::cast<Scene&>(scene); | |
| 47 | } | ||
| 48 | }; | ||
| 49 | |||
| 50 | 21 | Prim MakePrim(Scene& scene, PrimId id) { | |
| 51 | 42 | return {id, nb::find(&scene)}; | |
| 52 | 21 | } | |
| 53 | |||
| 54 | /// A prim, or the path of one. | ||
| 55 | using Target = std::variant<Prim, std::string>; | ||
| 56 | |||
| 57 | 56 | PrimId Resolve(const Scene& scene, const Target& target) { | |
| 58 | 56 | if (const Prim* prim = std::get_if<Prim>(&target)) { | |
| 59 | 42 | if (&prim->Owner() != &scene) { | |
| 60 | 1 | throw nb::value_error("The prim is another scene's"); | |
| 61 | } | ||
| 62 | 41 | if (!scene.Store().Alive(prim->id)) { | |
| 63 | 1 | throw nb::value_error("The prim has been removed"); | |
| 64 | } | ||
| 65 | 40 | return prim->id; | |
| 66 | } | ||
| 67 | 28 | return scene.Require(Path(std::get<std::string>(target))); | |
| 68 | } | ||
| 69 | |||
| 70 | /// @return A Python value as JSON: numbers, strings and None, and sequences of them nested. | ||
| 71 | 112 | Json ToJson(nb::handle value) { | |
| 72 | 112 | if (value.is_none()) { | |
| 73 | ✗ | return nullptr; | |
| 74 | } | ||
| 75 | 112 | if (nb::isinstance<nb::bool_>(value)) { | |
| 76 | 1 | return nb::cast<bool>(value); | |
| 77 | } | ||
| 78 | 111 | if (nb::isinstance<nb::int_>(value)) { | |
| 79 | 38 | return nb::cast<std::int64_t>(value); | |
| 80 | } | ||
| 81 | 73 | if (nb::isinstance<nb::float_>(value)) { | |
| 82 | 41 | return nb::cast<double>(value); | |
| 83 | } | ||
| 84 | 32 | if (nb::isinstance<nb::str>(value)) { | |
| 85 | 14 | return nb::cast<std::string>(value); | |
| 86 | } | ||
| 87 | 25 | if (nb::isinstance<nb::list>(value) || nb::isinstance<nb::tuple>(value)) { | |
| 88 | 25 | Json items = Json::array(); | |
| 89 | 111 | for (nb::handle item : value) { | |
| 90 | 86 | items.push_back(ToJson(item)); | |
| 91 | 25 | } | |
| 92 | 25 | return items; | |
| 93 | 25 | } | |
| 94 | // NumPy's scalars, which are no Python numbers. | ||
| 95 | ✗ | if (nb::hasattr(value, "__index__")) { | |
| 96 | ✗ | return nb::cast<std::int64_t>(value.attr("__index__")()); | |
| 97 | } | ||
| 98 | ✗ | if (nb::hasattr(value, "__float__")) { | |
| 99 | ✗ | return nb::cast<double>(value.attr("__float__")()); | |
| 100 | } | ||
| 101 | ✗ | throw nb::type_error( | |
| 102 | ✗ | ("A scene value cannot be a " + nb::cast<std::string>(value.type().attr("__name__"))) | |
| 103 | ✗ | .c_str()); | |
| 104 | } | ||
| 105 | |||
| 106 | /// @return JSON as Python values: arrays as lists. | ||
| 107 | 203 | nb::object ToPython(const Json& json) { | |
| 108 | 203 | switch (json.type()) { | |
| 109 | 1 | case Json::value_t::boolean: | |
| 110 | 1 | return nb::bool_(json.get<bool>()); | |
| 111 | 1 | case Json::value_t::number_integer: | |
| 112 | 1 | return nb::int_(json.get<std::int64_t>()); | |
| 113 | ✗ | case Json::value_t::number_unsigned: | |
| 114 | ✗ | return nb::int_(json.get<std::uint64_t>()); | |
| 115 | 150 | case Json::value_t::number_float: | |
| 116 | 150 | return nb::float_(json.get<double>()); | |
| 117 | 4 | case Json::value_t::string: | |
| 118 | 4 | return nb::str(json.get_ref<const std::string&>().c_str()); | |
| 119 | 47 | case Json::value_t::array: { | |
| 120 | 47 | nb::list items; | |
| 121 | 325 | for (const Json& item : json) { | |
| 122 | 368 | items.append(ToPython(item)); | |
| 123 | } | ||
| 124 | 47 | return items; | |
| 125 | 47 | } | |
| 126 | ✗ | default: | |
| 127 | ✗ | return nb::none(); | |
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | /// Runs `edit`, naming the prim and attribute in what it throws. | ||
| 132 | 34 | void Attempt(const Scene& scene, PrimId prim, std::string_view attribute, | |
| 133 | const std::function<void()>& edit) { | ||
| 134 | 34 | try { | |
| 135 | 34 | edit(); | |
| 136 | 2 | } catch (const std::exception& error) { | |
| 137 | 2 | const std::string message = | |
| 138 | 6 | "'" + scene.PrimPath(prim).String() + "' " + std::string(attribute) + ": "; | |
| 139 | 4 | throw nb::value_error((message + error.what()).c_str()); | |
| 140 | 2 | } | |
| 141 | 32 | } | |
| 142 | |||
| 143 | /// @return Whether a value is an array to share: a NumPy array or another DLPack producer. | ||
| 144 | 21 | bool IsArray(nb::handle value) { | |
| 145 | 21 | return nb::hasattr(value, "__dlpack__"); | |
| 146 | } | ||
| 147 | |||
| 148 | /// @return Whether DLPack places a tensor on the host or a GPU, with its device index. | ||
| 149 | ✗ | std::tuple<int, int> DlpackDevice(const Tensor& tensor) { | |
| 150 | ✗ | const Device device = tensor.Location(); | |
| 151 | ✗ | switch (device.kind) { | |
| 152 | ✗ | case DeviceKind::Cuda: | |
| 153 | ✗ | return {static_cast<int>(kDLCUDA), device.id}; | |
| 154 | ✗ | case DeviceKind::Rocm: | |
| 155 | ✗ | return {static_cast<int>(kDLROCM), device.id}; | |
| 156 | case DeviceKind::Cpu: | ||
| 157 | break; | ||
| 158 | } | ||
| 159 | ✗ | return {static_cast<int>(kDLCPU), 0}; | |
| 160 | } | ||
| 161 | |||
| 162 | /// An authored array, which NumPy, PyTorch and any DLPack library view in place. | ||
| 163 | 12 | struct SceneArray { | |
| 164 | Tensor tensor; | ||
| 165 | }; | ||
| 166 | |||
| 167 | 8 | nb::object TransformRows(const glm::dmat4& matrix) { | |
| 168 | 8 | return ToPython(ernie::ToJson(Value(matrix))); | |
| 169 | } | ||
| 170 | |||
| 171 | 23 | XformOps ReadXformArguments(const nb::object& translate, const nb::object& rotate, | |
| 172 | const std::string& rotationOrder, const nb::object& orient, | ||
| 173 | const nb::object& scale) { | ||
| 174 | 23 | Json xform = Json::object(); | |
| 175 | 115 | const auto add = [&](const char* key, const nb::object& value) { | |
| 176 | 92 | if (!value.is_none()) { | |
| 177 | 6 | xform[key] = ToJson(value); | |
| 178 | } | ||
| 179 | 92 | }; | |
| 180 | 23 | add("translate", translate); | |
| 181 | 23 | add("rotate", rotate); | |
| 182 | 23 | add("orient", orient); | |
| 183 | 23 | add("scale", scale); | |
| 184 | 23 | xform["rotationOrder"] = rotationOrder; | |
| 185 | 45 | return ReadXform(xform); | |
| 186 | 23 | } | |
| 187 | |||
| 188 | /// Authors an attribute from a Python value: an array is shared in place, anything else read as | ||
| 189 | /// scene files read it. | ||
| 190 | 21 | void SetValue(Scene& scene, PrimId prim, const std::string& attribute, const nb::handle& value) { | |
| 191 | 21 | if (IsArray(value)) { | |
| 192 | 2 | Tensor array = TakeTensor(nb::borrow(value), nb::none()); | |
| 193 | 2 | Attempt(scene, prim, attribute, [&] { scene.Write(prim, attribute, std::move(array)); }); | |
| 194 | 1 | return; | |
| 195 | 1 | } | |
| 196 | 20 | const Json json = ToJson(value); | |
| 197 | 41 | Attempt(scene, prim, attribute, [&] { LoadAttribute(scene, prim, attribute, json); }); | |
| 198 | 20 | } | |
| 199 | |||
| 200 | using Vector3 = std::array<double, 3>; | ||
| 201 | |||
| 202 | 8 | glm::dvec3 ToDvec3(const Vector3& v) { | |
| 203 | 8 | return {v[0], v[1], v[2]}; | |
| 204 | } | ||
| 205 | |||
| 206 | /// The `define` keywords that author transform ops rather than attributes. | ||
| 207 | constexpr std::array<std::string_view, 5> kXformKeywords{"translate", "rotate", "rotation_order", | ||
| 208 | "orient", "scale"}; | ||
| 209 | |||
| 210 | /// @return The transform ops `define`'s keywords author, or none when they author no op. | ||
| 211 | 19 | std::optional<XformOps> TakeXformKeywords(const nb::kwargs& keywords) { | |
| 212 | 114 | const auto keyword = [&](const char* name) { | |
| 213 | 98 | return keywords.contains(name) ? nb::object(keywords[name]) : nb::none(); | |
| 214 | 19 | }; | |
| 215 | 19 | const nb::object order = keyword("rotation_order"); | |
| 216 | 19 | XformOps xform = | |
| 217 | 38 | ReadXformArguments(keyword("translate"), keyword("rotate"), | |
| 218 | 39 | order.is_none() ? std::string("XYZ") : nb::cast<std::string>(order), | |
| 219 | 38 | keyword("orient"), keyword("scale")); | |
| 220 | 19 | if (!xform.translate && !xform.rotate && !xform.orient && !xform.scale) { | |
| 221 | 17 | return std::nullopt; | |
| 222 | } | ||
| 223 | 2 | return xform; | |
| 224 | 19 | } | |
| 225 | |||
| 226 | } // namespace | ||
| 227 | |||
| 228 | 6 | void BindScene(nb::module_& module) { | |
| 229 | 6 | nb::class_<Prim>(module, "Prim", "A prim of a scene, valid until it is removed.") | |
| 230 | 6 | .def("__eq__", | |
| 231 | 10 | [](const Prim& self, const Prim& other) { | |
| 232 | 8 | return self.id == other.id && self.scene.is(other.scene); | |
| 233 | }) | ||
| 234 | 6 | .def("__hash__", | |
| 235 | ✗ | [](const Prim& self) { | |
| 236 | ✗ | return (static_cast<std::uint64_t>(self.id.generation) << 32) | self.id.slot; | |
| 237 | }) | ||
| 238 | 6 | .def("__repr__", | |
| 239 | ✗ | [](const Prim& self) { | |
| 240 | ✗ | return "Prim(" + std::to_string(self.id.slot) + ", " + | |
| 241 | ✗ | std::to_string(self.id.generation) + ")"; | |
| 242 | }) | ||
| 243 | 6 | .def( | |
| 244 | "set", | ||
| 245 | 5 | [](const Prim& self, const std::string& attribute, const nb::object& value) { | |
| 246 | 5 | Scene& scene = self.Owner(); | |
| 247 | 5 | SetValue(scene, Resolve(scene, self), attribute, value); | |
| 248 | 5 | return self; | |
| 249 | }, | ||
| 250 | 12 | nb::arg("attribute"), nb::arg("value"), | |
| 251 | "Authors an attribute as `Scene.set` does, returning the prim to set more."); | ||
| 252 | |||
| 253 | 6 | nb::class_<SceneArray>(module, "SceneArray", | |
| 254 | "An array attribute's values, shared in place through DLPack.") | ||
| 255 | 7 | .def_prop_ro("shape", [](const SceneArray& self) { return self.tensor.Shape(); }) | |
| 256 | 6 | .def( | |
| 257 | "__dlpack__", | ||
| 258 | 2 | [](const SceneArray& self, const nb::object&, const nb::object& maxVersion, | |
| 259 | const nb::object&, const nb::object& copy) { | ||
| 260 | 2 | if (!copy.is_none() && nb::cast<bool>(copy)) { | |
| 261 | ✗ | throw nb::buffer_error("Scene arrays are shared in place, never copied"); | |
| 262 | } | ||
| 263 | 2 | const bool versioned = | |
| 264 | 4 | !maxVersion.is_none() && nb::cast<std::tuple<int, int>>(maxVersion) >= | |
| 265 | 2 | std::tuple<int, int>(DLPACK_MAJOR_VERSION, 0); | |
| 266 | 2 | return Capsule(self.tensor.ToDlpack(), versioned); | |
| 267 | }, | ||
| 268 | 12 | nb::kw_only(), nb::arg("stream") = nb::none(), nb::arg("max_version") = nb::none(), | |
| 269 | 12 | nb::arg("dl_device") = nb::none(), nb::arg("copy") = nb::none()) | |
| 270 | 6 | .def("__dlpack_device__", [](const SceneArray& self) { return DlpackDevice(self.tensor); }); | |
| 271 | |||
| 272 | 6 | nb::class_<Scene>(module, "Scene", "Typed prims at paths, their attributes in columns by type.") | |
| 273 | 12 | .def(nb::init<>()) | |
| 274 | 6 | .def( | |
| 275 | "define", | ||
| 276 | 20 | [](Scene& self, const std::string& path, const std::string& type, | |
| 277 | const nb::kwargs& attributes) { | ||
| 278 | 21 | const PrimId prim = self.Define(Path(path), type); | |
| 279 | 25 | for (const auto& [key, value] : attributes) { | |
| 280 | 6 | const std::string name = nb::cast<std::string>(key); | |
| 281 | 6 | if (std::ranges::find(kXformKeywords, name) == kXformKeywords.end()) { | |
| 282 | 3 | SetValue(self, prim, name, value); | |
| 283 | } | ||
| 284 | 6 | } | |
| 285 | 19 | if (const std::optional<XformOps> xform = TakeXformKeywords(attributes)) { | |
| 286 | 4 | SetXform(self, Path(path), *xform); | |
| 287 | } | ||
| 288 | 19 | return MakePrim(self, prim); | |
| 289 | }, | ||
| 290 | 18 | nb::arg("path"), nb::arg("type") = "", nb::arg("attributes"), | |
| 291 | "Defines a prim of a concrete type, or none, with its missing ancestors. Keywords " | ||
| 292 | "author its transform ops (translate, rotate, rotation_order, orient, scale, as " | ||
| 293 | "`set_xform`) and attributes by name (radius=2.0), as `set` does.") | ||
| 294 | 6 | .def( | |
| 295 | 2 | "remove", [](Scene& self, const std::string& path) { return self.Remove(Path(path)); }, | |
| 296 | 6 | nb::arg("path"), "Removes a prim and its descendants; whether there was one.") | |
| 297 | 6 | .def( | |
| 298 | "find", | ||
| 299 | 4 | [](Scene& self, const std::string& path) -> std::optional<Prim> { | |
| 300 | 4 | const std::optional<PrimId> prim = self.Find(Path(path)); | |
| 301 | 4 | return prim ? std::optional<Prim>(MakePrim(self, *prim)) : std::nullopt; | |
| 302 | }, | ||
| 303 | 6 | nb::arg("path"), "The prim at a path, or None.") | |
| 304 | 6 | .def("__contains__", | |
| 305 | 2 | [](const Scene& self, const std::string& path) { return self.Has(Path(path)); }) | |
| 306 | 6 | .def( | |
| 307 | "path", | ||
| 308 | 1 | [](const Scene& self, const Target& prim) { | |
| 309 | 2 | return self.PrimPath(Resolve(self, prim)).String(); | |
| 310 | }, | ||
| 311 | 6 | nb::arg("prim"), "A prim's path.") | |
| 312 | 6 | .def( | |
| 313 | "type_name", | ||
| 314 | 4 | [](const Scene& self, const Target& prim) { | |
| 315 | 4 | return std::string(self.TypeName(Resolve(self, prim))); | |
| 316 | }, | ||
| 317 | 6 | nb::arg("prim"), "A prim's type; empty for none.") | |
| 318 | 6 | .def( | |
| 319 | "children", | ||
| 320 | 7 | [](const Scene& self, const std::string& path) { | |
| 321 | 1 | return Map(self.Children(Path(path)), &Path::String); | |
| 322 | }, | ||
| 323 | 18 | nb::arg("path") = "/", "The paths of a prim's children.") | |
| 324 | 6 | .def( | |
| 325 | "prims", | ||
| 326 | 7 | [](const Scene& self, const std::string& type) { | |
| 327 | 1 | return Map(self.Prims(type), &Path::String); | |
| 328 | }, | ||
| 329 | 18 | nb::arg("type") = "", "The paths of every prim of a type or one deriving from it.") | |
| 330 | 6 | .def( | |
| 331 | "apply_api", | ||
| 332 | 1 | [](Scene& self, const Target& prim, const std::string& api) { | |
| 333 | 1 | self.ApplyApi(Resolve(self, prim), api); | |
| 334 | 1 | }, | |
| 335 | 6 | nb::arg("prim"), nb::arg("api"), | |
| 336 | "Applies an API schema, or an instance of one, as 'CollectionAPI:lights'.") | ||
| 337 | 6 | .def( | |
| 338 | "attributes", | ||
| 339 | 1 | [](const Scene& self, const Target& prim) { | |
| 340 | 2 | return self.Attributes(self.PrimPath(Resolve(self, prim))); | |
| 341 | }, | ||
| 342 | 6 | nb::arg("prim"), "The authored attributes' names.") | |
| 343 | 6 | .def( | |
| 344 | "set", | ||
| 345 | 14 | [](Scene& self, const Target& target, const std::string& attribute, | |
| 346 | const nb::object& value) { | ||
| 347 | 14 | SetValue(self, Resolve(self, target), attribute, value); | |
| 348 | 12 | }, | |
| 349 | 6 | nb::arg("prim"), nb::arg("attribute"), nb::arg("value"), | |
| 350 | "Authors an attribute: a number, string, sequence for a vector, quaternion " | ||
| 351 | "(w, x, y, z), matrix (4 rows) or array, list of strings for tokens or paths, or a " | ||
| 352 | "NumPy or DLPack array, shared in place.") | ||
| 353 | 6 | .def( | |
| 354 | "get", | ||
| 355 | 15 | [](const Scene& self, const Target& target, | |
| 356 | const std::string& attribute) -> nb::object { | ||
| 357 | 15 | const PrimId prim = Resolve(self, target); | |
| 358 | 14 | const std::optional<Value> value = self.Get(prim, attribute); | |
| 359 | 14 | if (!value) { | |
| 360 | 3 | return nb::none(); | |
| 361 | } | ||
| 362 | 11 | nb::object result; | |
| 363 | 22 | Attempt(self, prim, attribute, [&] { result = ToPython(ernie::ToJson(*value)); }); | |
| 364 | 11 | return result; | |
| 365 | 25 | }, | |
| 366 | 6 | nb::arg("prim"), nb::arg("attribute"), | |
| 367 | "An authored value as scene files write it, or None; arrays are read with `read`.") | ||
| 368 | 6 | .def( | |
| 369 | "write", | ||
| 370 | 2 | [](Scene& self, const Target& target, const std::string& attribute, | |
| 371 | const nb::object& array, const nb::object& stream) { | ||
| 372 | 2 | const PrimId prim = Resolve(self, target); | |
| 373 | 2 | Tensor tensor = TakeTensor(array, stream); | |
| 374 | 3 | Attempt(self, prim, attribute, | |
| 375 | 4 | [&] { self.Write(prim, attribute, std::move(tensor)); }); | |
| 376 | 2 | }, | |
| 377 | 12 | nb::arg("prim"), nb::arg("attribute"), nb::arg("array"), nb::arg("stream") = nb::none(), | |
| 378 | "Authors an array attribute with a NumPy or DLPack array, shared, not copied.") | ||
| 379 | 6 | .def( | |
| 380 | "read", | ||
| 381 | 4 | [](const Scene& self, const Target& target, | |
| 382 | const std::string& attribute) -> std::optional<SceneArray> { | ||
| 383 | 4 | const Tensor* array = self.Read(Resolve(self, target), attribute); | |
| 384 | 7 | return array != nullptr ? std::optional<SceneArray>(SceneArray{*array}) | |
| 385 | 4 | : std::nullopt; | |
| 386 | }, | ||
| 387 | 6 | nb::arg("prim"), nb::arg("attribute"), | |
| 388 | "An authored array, shared in place through DLPack (numpy.from_dlpack), or None.") | ||
| 389 | 6 | .def( | |
| 390 | "clear", | ||
| 391 | 1 | [](Scene& self, const Target& prim, const std::string& attribute) { | |
| 392 | 1 | return self.Clear(Resolve(self, prim), attribute); | |
| 393 | }, | ||
| 394 | 6 | nb::arg("prim"), nb::arg("attribute"), "Unauthors an attribute; whether it was.") | |
| 395 | 6 | .def( | |
| 396 | "set_xform", | ||
| 397 | 4 | [](Scene& self, const Target& target, const nb::object& translate, | |
| 398 | const nb::object& rotate, const std::string& rotationOrder, const nb::object& orient, | ||
| 399 | const nb::object& scale) { | ||
| 400 | 4 | const Path path = self.PrimPath(Resolve(self, target)); | |
| 401 | 3 | SetXform(self, path, | |
| 402 | 7 | ReadXformArguments(translate, rotate, rotationOrder, orient, scale)); | |
| 403 | 3 | }, | |
| 404 | 12 | nb::arg("prim"), nb::kw_only(), nb::arg("translate") = nb::none(), | |
| 405 | 18 | nb::arg("rotate") = nb::none(), nb::arg("rotation_order") = "XYZ", | |
| 406 | 12 | nb::arg("orient") = nb::none(), nb::arg("scale") = nb::none(), | |
| 407 | "Authors a prim's transform ops: translate, rotate (degrees, in `rotation_order`) or " | ||
| 408 | "orient (w, x, y, z), and scale.") | ||
| 409 | 6 | .def( | |
| 410 | "local_transform", | ||
| 411 | 2 | [](const Scene& self, const Target& prim) { | |
| 412 | 2 | return TransformRows(self.LocalTransform(Resolve(self, prim))); | |
| 413 | }, | ||
| 414 | 6 | nb::arg("prim"), "A prim's own transform, as 4 rows.") | |
| 415 | 6 | .def( | |
| 416 | "world_transform", | ||
| 417 | 2 | [](const Scene& self, const Target& prim) { | |
| 418 | 2 | return TransformRows(self.WorldTransform(Resolve(self, prim))); | |
| 419 | }, | ||
| 420 | 6 | nb::arg("prim"), | |
| 421 | "A prim's transform to world space, as 4 rows: column vectors, translation in the " | ||
| 422 | "last column.") | ||
| 423 | 6 | .def("update", &Scene::Update, | |
| 424 | "Brings derived data, such as inherited attributes and extents, up to date.") | ||
| 425 | 6 | .def( | |
| 426 | "load_json", | ||
| 427 | 2 | [](Scene& self, std::string_view text, const std::vector<nb::object>& arrays) { | |
| 428 | 2 | const std::vector<Tensor> tensors = Map( | |
| 429 | 4 | arrays, [](const nb::object& array) { return TakeTensor(array, nb::none()); }); | |
| 430 | 2 | LoadScene(self, text, tensors); | |
| 431 | 2 | }, | |
| 432 | 18 | nb::arg("text"), nb::arg("arrays") = std::vector<nb::object>(), | |
| 433 | "Builds the prims a scene file's text describes; {\"$array\": i} names arrays[i], " | ||
| 434 | "shared in place.") | ||
| 435 | 6 | .def("save_json", &SaveScene, "The scene as a scene file's text.") | |
| 436 | 6 | .def( | |
| 437 | "load", | ||
| 438 | 2 | [](Scene& self, const std::filesystem::path& path) { LoadSceneFile(self, path); }, | |
| 439 | 6 | nb::arg("path"), "Builds the prims a scene file describes.") | |
| 440 | 6 | .def( | |
| 441 | "save", | ||
| 442 | 1 | [](const Scene& self, const std::filesystem::path& path) { SaveSceneFile(self, path); }, | |
| 443 | 12 | nb::arg("path"), "Writes the scene to a scene file."); | |
| 444 | |||
| 445 | // Each prim type by its name, for `scene.define(path, ernie.Sphere)`. | ||
| 446 | 6 | nb::list types; | |
| 447 | 396 | for (const SchemaDef* schema : Schemas()) { | |
| 448 | 390 | if (schema->kind == SchemaKind::Concrete) { | |
| 449 | 234 | const std::string name(schema->name); | |
| 450 | 234 | module.attr(name.c_str()) = name; | |
| 451 | 234 | types.append(name); | |
| 452 | 234 | } | |
| 453 | } | ||
| 454 | 12 | module.attr("PRIM_TYPES") = nb::tuple(types); | |
| 455 | |||
| 456 | 6 | nb::module_ names = module.def_submodule("tokens", "Names the scene's code shares."); | |
| 457 | 12 | names.attr("TRANSFORM") = std::string(tokens::kTransform); | |
| 458 | 12 | names.attr("TRANSLATE") = std::string(tokens::kTranslate); | |
| 459 | 12 | names.attr("ORIENT") = std::string(tokens::kOrient); | |
| 460 | 12 | names.attr("SCALE") = std::string(tokens::kScale); | |
| 461 | 12 | names.attr("XFORM_OP_ORDER") = std::string(tokens::kXformOpOrder); | |
| 462 | |||
| 463 | 6 | module.def( | |
| 464 | "look_at_transform", | ||
| 465 | 8 | [](const Vector3& eye, const Vector3& target, const Vector3& up) { | |
| 466 | 2 | return TransformRows(LookAtTransform(ToDvec3(eye), ToDvec3(target), ToDvec3(up))); | |
| 467 | }, | ||
| 468 | 12 | nb::arg("eye"), nb::arg("target"), nb::arg("up") = Vector3{0.0, 0.0, 1.0}, | |
| 469 | "A camera's transform at `eye` looking at `target`, as 4 rows: cameras look down -z."); | ||
| 470 | 6 | module.def( | |
| 471 | "distant_light_transform", | ||
| 472 | 8 | [](const Vector3& towardLight) { | |
| 473 | 2 | return TransformRows(DistantLightTransform(ToDvec3(towardLight))); | |
| 474 | }, | ||
| 475 | 6 | nb::arg("toward_light"), | |
| 476 | "A distant light's transform shining from the direction to its source, as 4 rows."); | ||
| 477 | 6 | } | |
| 478 | |||
| 479 | } // namespace ernie::python | ||
| 480 |