scene/xform.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file xform.cpp | ||
| 3 | /// @brief Transform op composition, after UsdGeomXformable. | ||
| 4 | #include "ernie/scene/xform.h" | ||
| 5 | |||
| 6 | #include "ernie/scene/convert.h" | ||
| 7 | #include "ernie/scene/tokens.h" | ||
| 8 | |||
| 9 | #include <glm/gtc/matrix_transform.hpp> | ||
| 10 | |||
| 11 | #include <algorithm> | ||
| 12 | #include <array> | ||
| 13 | #include <cmath> | ||
| 14 | #include <stdexcept> | ||
| 15 | #include <string> | ||
| 16 | #include <vector> | ||
| 17 | |||
| 18 | namespace ernie { | ||
| 19 | namespace { | ||
| 20 | |||
| 21 | constexpr std::array<std::array<int, 3>, 6> kAxes{ | ||
| 22 | {{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}}; | ||
| 23 | constexpr std::array<std::string_view, 6> kRotateOps{"xformOp:rotateXYZ", "xformOp:rotateXZY", | ||
| 24 | "xformOp:rotateYXZ", "xformOp:rotateYZX", | ||
| 25 | "xformOp:rotateZXY", "xformOp:rotateZYX"}; | ||
| 26 | |||
| 27 | /// The axes an order turns about, first first. | ||
| 28 | 11451 | std::array<int, 3> TurnAxes(RotationOrder order) { | |
| 29 | 11451 | return kAxes[static_cast<std::size_t>(order)]; | |
| 30 | } | ||
| 31 | |||
| 32 | /// A right-handed turn about one axis, built from its cosine and sine so the axis stays exact. | ||
| 33 | 19962 | glm::dmat3 AxisRotation(int axis, double degrees) { | |
| 34 | 19962 | const double c = std::cos(glm::radians(degrees)); | |
| 35 | 19962 | const double s = std::sin(glm::radians(degrees)); | |
| 36 | 19962 | const int a = (axis + 1) % 3; | |
| 37 | 19962 | const int b = (axis + 2) % 3; | |
| 38 | 19962 | glm::dmat3 r(1.0); | |
| 39 | 19962 | r[a][a] = c; | |
| 40 | 19962 | r[a][b] = s; | |
| 41 | 19962 | r[b][a] = -s; | |
| 42 | 19962 | r[b][b] = c; | |
| 43 | 19962 | return r; | |
| 44 | } | ||
| 45 | |||
| 46 | /// The op types, as `XformOpKind` orders them. | ||
| 47 | constexpr std::array<std::string_view, 13> kOpTypes{ | ||
| 48 | "translate", "scale", "rotateX", "rotateY", "rotateZ", "rotateXYZ", "rotateXZY", | ||
| 49 | "rotateYXZ", "rotateYZX", "rotateZXY", "rotateZYX", "orient", "transform"}; | ||
| 50 | |||
| 51 | /// One entry of `xformOpOrder`, its attribute viewing the order's text. | ||
| 52 | struct OpEntry { | ||
| 53 | std::string_view attribute; | ||
| 54 | XformOpKind kind = XformOpKind::Transform; | ||
| 55 | bool invert = false; | ||
| 56 | }; | ||
| 57 | |||
| 58 | 3814 | OpEntry ParseOp(const Scene& scene, PrimId prim, std::string_view entry) { | |
| 59 | 3814 | OpEntry op; | |
| 60 | 3814 | if (entry.starts_with(tokens::kInvertPrefix)) { | |
| 61 | 7 | op.invert = true; | |
| 62 | 7 | entry.remove_prefix(tokens::kInvertPrefix.size()); | |
| 63 | } | ||
| 64 | 3814 | if (!entry.starts_with(tokens::kXformOpPrefix)) { | |
| 65 | 2 | throw std::invalid_argument("xformOpOrder of '" + scene.PrimPath(prim).String() + | |
| 66 | 5 | "' lists '" + std::string(entry) + "', which is no xformOp"); | |
| 67 | } | ||
| 68 | 3813 | const std::optional<XformOpKind> kind = ParseXformOpKind(entry); | |
| 69 | 3813 | if (!kind) { | |
| 70 | 6 | throw std::invalid_argument("'" + scene.PrimPath(prim).String() + "' has transform op " + | |
| 71 | 8 | std::string(entry) + ", which Ernie does not know"); | |
| 72 | } | ||
| 73 | 3811 | op.attribute = entry; | |
| 74 | 3811 | op.kind = *kind; | |
| 75 | 3811 | return op; | |
| 76 | } | ||
| 77 | |||
| 78 | /// The order's ops, outermost first, without its reset token; `xformOp:transform` without one. | ||
| 79 | 3571 | template <typename Visit> void ForEachOp(const Scene& scene, PrimId prim, Visit visit) { | |
| 80 | 3571 | const std::optional<Value> order = scene.Get(prim, tokens::kXformOpOrder); | |
| 81 | 3571 | if (!order) { | |
| 82 | 73 | visit(OpEntry{tokens::kTransform, XformOpKind::Transform, false}); | |
| 83 | 73 | return; | |
| 84 | } | ||
| 85 | 10815 | for (const std::string& entry : *order->Get<std::vector<std::string>>()) { | |
| 86 | 3825 | if (entry != tokens::kResetXformStack) { | |
| 87 | 3814 | visit(ParseOp(scene, prim, entry)); | |
| 88 | } | ||
| 89 | } | ||
| 90 | 3565 | } | |
| 91 | |||
| 92 | 3 | std::invalid_argument BadOp(const Scene& scene, PrimId prim, const OpEntry& op, | |
| 93 | std::string_view need) { | ||
| 94 | 12 | return std::invalid_argument(std::string(op.attribute) + " of '" + | |
| 95 | 12 | scene.PrimPath(prim).String() + "' must be " + std::string(need)); | |
| 96 | } | ||
| 97 | |||
| 98 | 4703 | glm::dvec3 ToVec3(const Scene& scene, PrimId prim, const OpEntry& op, const Value& value, | |
| 99 | bool scalar) { | ||
| 100 | 4703 | if (const auto vector = ToVector<3>(value)) { | |
| 101 | 4700 | return *vector; | |
| 102 | } | ||
| 103 | 3 | if (const auto number = ToNumber(value); scalar && number) { | |
| 104 | 2 | return glm::dvec3(*number); | |
| 105 | } | ||
| 106 | 2 | throw BadOp(scene, prim, op, scalar ? "three numbers or one" : "three numbers"); | |
| 107 | } | ||
| 108 | |||
| 109 | /// The matrix of one op from its value, inverted if the order says so; identity without one. | ||
| 110 | 4831 | glm::dmat4 OpMatrix(const Scene& scene, PrimId prim, const OpEntry& op, | |
| 111 | const std::optional<Value>& value) { | ||
| 112 | 4831 | if (!value) { | |
| 113 | 70 | return glm::dmat4(1.0); | |
| 114 | } | ||
| 115 | 4761 | const auto kind = static_cast<std::uint8_t>(op.kind); | |
| 116 | 4761 | glm::dmat4 m(1.0); | |
| 117 | 4761 | switch (op.kind) { | |
| 118 | 4326 | case XformOpKind::Translate: | |
| 119 | 4326 | m = glm::translate(m, ToVec3(scene, prim, op, *value, false)); | |
| 120 | 4325 | break; | |
| 121 | 349 | case XformOpKind::Scale: | |
| 122 | 349 | m = glm::scale(m, ToVec3(scene, prim, op, *value, true)); | |
| 123 | 349 | break; | |
| 124 | 10 | case XformOpKind::RotateX: | |
| 125 | 10 | case XformOpKind::RotateY: | |
| 126 | 10 | case XformOpKind::RotateZ: { | |
| 127 | 10 | const std::optional<double> degrees = ToNumber(*value); | |
| 128 | 10 | if (!degrees) { | |
| 129 | 1 | throw BadOp(scene, prim, op, "one angle in degrees"); | |
| 130 | } | ||
| 131 | 9 | m = glm::dmat4( | |
| 132 | 9 | AxisRotation(kind - static_cast<std::uint8_t>(XformOpKind::RotateX), *degrees)); | |
| 133 | 9 | break; | |
| 134 | } | ||
| 135 | 34 | case XformOpKind::Orient: { | |
| 136 | 34 | const auto* q = value->Get<glm::dquat>(); | |
| 137 | 34 | if (q == nullptr) { | |
| 138 | 1 | throw BadOp(scene, prim, op, "a quaternion"); | |
| 139 | } | ||
| 140 | 33 | m = glm::dmat4(glm::mat3_cast(glm::normalize(*q))); | |
| 141 | 33 | break; | |
| 142 | } | ||
| 143 | 14 | case XformOpKind::Transform: { | |
| 144 | 14 | const auto* matrix = value->Get<glm::dmat4>(); | |
| 145 | 14 | if (matrix == nullptr) { | |
| 146 | ✗ | throw BadOp(scene, prim, op, "a 4 x 4 matrix"); | |
| 147 | } | ||
| 148 | 14 | m = *matrix; | |
| 149 | 14 | break; | |
| 150 | } | ||
| 151 | 28 | default: | |
| 152 | 28 | m = glm::dmat4(EulerRotation( | |
| 153 | 56 | ToVec3(scene, prim, op, *value, false), | |
| 154 | 28 | static_cast<RotationOrder>(kind - static_cast<std::uint8_t>(XformOpKind::RotateXYZ)))); | |
| 155 | } | ||
| 156 | 4758 | return op.invert ? glm::inverse(m) : m; | |
| 157 | } | ||
| 158 | |||
| 159 | } // namespace | ||
| 160 | |||
| 161 | 77 | std::string_view RotateOp(RotationOrder order) { | |
| 162 | 77 | return kRotateOps[static_cast<std::size_t>(order)]; | |
| 163 | } | ||
| 164 | |||
| 165 | 6651 | glm::dmat3 EulerRotation(const glm::dvec3& degrees, RotationOrder order) { | |
| 166 | 6651 | const auto [i, j, k] = TurnAxes(order); | |
| 167 | 6651 | return AxisRotation(k, degrees[k]) * AxisRotation(j, degrees[j]) * AxisRotation(i, degrees[i]); | |
| 168 | } | ||
| 169 | |||
| 170 | 4800 | glm::dvec3 EulerAngles(const glm::dmat3& rotation, RotationOrder order) { | |
| 171 | 4800 | const auto [i, j, k] = TurnAxes(order); | |
| 172 | // +1 for the cyclic orders (XYZ, YZX, ZXY), -1 for the others. | ||
| 173 | 4800 | const double s = (j == (i + 1) % 3) ? 1.0 : -1.0; | |
| 174 | 32400 | const auto at = [&](int row, int col) { return rotation[col][row]; }; | |
| 175 | 4800 | const double sinMiddle = std::clamp(-s * at(k, i), -1.0, 1.0); | |
| 176 | 4800 | double first = 0.0; | |
| 177 | 4800 | double last = 0.0; | |
| 178 | 4800 | if (std::abs(sinMiddle) < 1.0 - 1e-12) { | |
| 179 | 4200 | first = std::atan2(s * at(k, j), at(k, k)); | |
| 180 | 4200 | last = std::atan2(s * at(j, i), at(i, i)); | |
| 181 | } else { | ||
| 182 | // Gimbal lock: the first and last axes line up, so the first takes the whole turn. | ||
| 183 | 600 | first = std::atan2(-s * at(j, k), at(j, j)); | |
| 184 | } | ||
| 185 | 4800 | glm::dvec3 degrees(0.0); | |
| 186 | 4800 | degrees[i] = glm::degrees(first); | |
| 187 | // atan2 against the cosine stays accurate near lock, where asin magnifies rounding. | ||
| 188 | 4800 | degrees[j] = glm::degrees(std::atan2(sinMiddle, std::hypot(at(k, j), at(k, k)))); | |
| 189 | 4800 | degrees[k] = glm::degrees(last); | |
| 190 | 4800 | return degrees; | |
| 191 | } | ||
| 192 | |||
| 193 | 1541 | void SetXform(Scene& scene, const Path& path, const XformOps& xform) { | |
| 194 | 1541 | if (xform.rotate && xform.orient) { | |
| 195 | 1 | throw std::invalid_argument("An xform takes rotate or orient, not both"); | |
| 196 | } | ||
| 197 | 1540 | const PrimId prim = scene.Require(path); | |
| 198 | 1539 | std::vector<std::string> order; | |
| 199 | 1539 | if (ResetsXformStack(scene, prim)) { | |
| 200 | 1 | order.emplace_back(tokens::kResetXformStack); | |
| 201 | } | ||
| 202 | 3222 | const auto add = [&](std::string_view op, const Value& value) { | |
| 203 | 1683 | scene.Set(prim, op, value); | |
| 204 | 1683 | order.emplace_back(op); | |
| 205 | 3222 | }; | |
| 206 | 1539 | if (xform.translate) { | |
| 207 | 3056 | add(tokens::kTranslate, *xform.translate); | |
| 208 | } | ||
| 209 | 1539 | if (xform.rotate) { | |
| 210 | 18 | add(RotateOp(xform.rotationOrder), *xform.rotate); | |
| 211 | } | ||
| 212 | 1539 | if (xform.orient) { | |
| 213 | 144 | add(tokens::kOrient, glm::normalize(*xform.orient)); | |
| 214 | } | ||
| 215 | 1539 | if (xform.scale) { | |
| 216 | 148 | add(tokens::kScale, *xform.scale); | |
| 217 | } | ||
| 218 | 1539 | scene.Set(prim, tokens::kXformOpOrder, std::move(order)); | |
| 219 | 1539 | } | |
| 220 | |||
| 221 | 3 | glm::dmat4 LookAtTransform(const glm::dvec3& eye, const glm::dvec3& target, const glm::dvec3& up) { | |
| 222 | 3 | const glm::dvec3 forward = target - eye; | |
| 223 | 3 | if (glm::length(glm::cross(forward, up)) == 0.0) { | |
| 224 | ✗ | throw std::invalid_argument("A camera looking at its own eye or along its up vector"); | |
| 225 | } | ||
| 226 | 3 | return glm::inverse(glm::lookAtRH(eye, target, up)); | |
| 227 | } | ||
| 228 | |||
| 229 | 3 | glm::dmat4 DistantLightTransform(const glm::dvec3& towardLight) { | |
| 230 | 3 | if (glm::length(towardLight) == 0.0) { | |
| 231 | ✗ | throw std::invalid_argument("A distant light shining from no direction"); | |
| 232 | } | ||
| 233 | 3 | const glm::dvec3 z = glm::normalize(towardLight); | |
| 234 | // Any axis not along z completes the frame; the light is the same turned about it. | ||
| 235 | 3 | const glm::dvec3 helper = std::abs(z.z) < 0.9 ? glm::dvec3(0, 0, 1) : glm::dvec3(1, 0, 0); | |
| 236 | 3 | const glm::dvec3 x = glm::normalize(glm::cross(helper, z)); | |
| 237 | 3 | const glm::dvec3 y = glm::cross(z, x); | |
| 238 | 3 | return glm::dmat4(glm::dvec4(x, 0), glm::dvec4(y, 0), glm::dvec4(z, 0), glm::dvec4(0, 0, 0, 1)); | |
| 239 | } | ||
| 240 | |||
| 241 | 213 | glm::dmat4 ComposeXformOps(const Scene& scene, PrimId prim) { | |
| 242 | 213 | glm::dmat4 local(1.0); | |
| 243 | 213 | ForEachOp(scene, prim, [&](const OpEntry& op) { | |
| 244 | 254 | local = local * OpMatrix(scene, prim, op, scene.Get(prim, op.attribute)); | |
| 245 | 251 | }); | |
| 246 | 207 | return local; | |
| 247 | } | ||
| 248 | |||
| 249 | 5033 | bool ResetsXformStack(const Scene& scene, PrimId prim) { | |
| 250 | 5033 | const std::optional<Value> order = scene.Get(prim, tokens::kXformOpOrder); | |
| 251 | 5033 | const auto* entries = order ? order->Get<std::vector<std::string>>() : nullptr; | |
| 252 | 8453 | return entries != nullptr && !entries->empty() && entries->front() == tokens::kResetXformStack; | |
| 253 | 5033 | } | |
| 254 | |||
| 255 | 3819 | std::optional<XformOpKind> ParseXformOpKind(std::string_view attribute) { | |
| 256 | 3819 | if (!attribute.starts_with(tokens::kXformOpPrefix)) { | |
| 257 | ✗ | return std::nullopt; | |
| 258 | } | ||
| 259 | 3819 | std::string_view type = attribute.substr(tokens::kXformOpPrefix.size()); | |
| 260 | 3819 | type = type.substr(0, type.find(':')); | |
| 261 | 3819 | const auto found = std::ranges::find(kOpTypes, type); | |
| 262 | 3819 | if (found == kOpTypes.end()) { | |
| 263 | 2 | return std::nullopt; | |
| 264 | } | ||
| 265 | 3817 | return static_cast<XformOpKind>(found - kOpTypes.begin()); | |
| 266 | } | ||
| 267 | |||
| 268 | 3358 | std::vector<XformOp> XformOpsInOrder(const Scene& scene, PrimId prim) { | |
| 269 | 3358 | const TokenTable& tokens = scene.Store().Tokens(); | |
| 270 | 3358 | std::vector<XformOp> ops; | |
| 271 | 3358 | ForEachOp(scene, prim, [&](const OpEntry& op) { | |
| 272 | 7260 | ops.push_back({std::string(op.attribute), op.kind, op.invert, tokens.Find(op.attribute)}); | |
| 273 | 7260 | }); | |
| 274 | 3358 | return ops; | |
| 275 | ✗ | } | |
| 276 | |||
| 277 | 4577 | glm::dmat4 XformOpMatrix(const Scene& scene, PrimId prim, const XformOp& op) { | |
| 278 | // An attribute first authored after the order was read has no token in the op. | ||
| 279 | 4577 | return OpMatrix(scene, prim, {op.attribute, op.kind, op.invert}, | |
| 280 | 9154 | op.token ? scene.Get(prim, *op.token) : scene.Get(prim, op.attribute)); | |
| 281 | } | ||
| 282 | |||
| 283 | } // namespace ernie | ||
| 284 |