shaders/scene/transform.slang
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file transform.slang | ||
| 3 | /// @brief The transform graph on the device: programs of ops evaluated into local transforms, | ||
| 4 | /// composed level by level into worlds relative to the render origin, all in double-float, and | ||
| 5 | /// the worlds read back as floats by the kernels that place things. The programs' layout is | ||
| 6 | /// shared with C++ (transform_graph.h). | ||
| 7 | /// | ||
| 8 | /// import transform; | ||
| 9 | /// Affine world = LoadWorld(parameters.worlds, slot); | ||
| 10 | #ifdef __cplusplus | ||
| 11 | #pragma once | ||
| 12 | #include "ernie/shaders/shared.h" | ||
| 13 | namespace ernie::shaders { | ||
| 14 | #else | ||
| 15 | import affine; | ||
| 16 | import double_float; | ||
| 17 | import buffer_ref; | ||
| 18 | #endif | ||
| 19 | |||
| 20 | /// A program's op kinds: a folded constant, then each of the host's transform op kinds after it, | ||
| 21 | /// in its order; the Euler rotations run from `kRotateXYZ` to `kOrient`. | ||
| 22 | static const uint kConstant = 0; | ||
| 23 | static const uint kTranslate = 1; | ||
| 24 | static const uint kScale = 2; | ||
| 25 | static const uint kRotateX = 3; | ||
| 26 | static const uint kRotateXYZ = 6; | ||
| 27 | static const uint kOrient = 12; | ||
| 28 | |||
| 29 | /// An op's flags: bit 0 inverts it; bits 8 to 15 count the floats it reads. | ||
| 30 | static const uint kInvert = 1; | ||
| 31 | static const uint kFloatsShift = 8; | ||
| 32 | |||
| 33 | /// The most ops a program holds: device ops and the folded host ops between them. | ||
| 34 | static const uint kMaxOps = 8; | ||
| 35 | /// No slot: a root's parent. | ||
| 36 | static const uint kNone = 0xffffffff; | ||
| 37 | |||
| 38 | /// A transform's top three rows in double-float, as the device keeps them: each value the sum of | ||
| 39 | /// its high float and its low one. | ||
| 40 | struct SplitRows { | ||
| 41 | 37218 | float4 high[3]; | |
| 42 | 35775 | float4 low[3]; | |
| 43 | #ifdef __cplusplus | ||
| 44 | 86189 | bool operator==(const SplitRows&) const = default; | |
| 45 | #endif | ||
| 46 | }; | ||
| 47 | |||
| 48 | /// One op of a program. | ||
| 49 | 2296 | struct Op { | |
| 50 | 9139 | uint kind; | |
| 51 | 8852 | uint flags; | |
| 52 | /// A bound op's row of floats. | ||
| 53 | 8852 | BufferRef<float> source; | |
| 54 | /// A folded constant, exactly as the host split it. | ||
| 55 | 8848 | SplitRows constant; | |
| 56 | #ifdef __cplusplus | ||
| 57 | 9139 | bool operator==(const Op&) const = default; | |
| 58 | #endif | ||
| 59 | }; | ||
| 60 | |||
| 61 | struct LocalParameters { | ||
| 62 | /// Each programmed node's slot, program of `kMaxOps` ops and its length. | ||
| 63 | BufferRef<uint> slots; | ||
| 64 | BufferRef<Op> programs; | ||
| 65 | BufferRef<uint> lengths; | ||
| 66 | /// The locals, by slot. | ||
| 67 | BufferRef<float4> locals; | ||
| 68 | uint count; | ||
| 69 | }; | ||
| 70 | |||
| 71 | struct ComposeParameters { | ||
| 72 | /// The level's slots. | ||
| 73 | BufferRef<uint> slots; | ||
| 74 | /// Each slot's parent, or `kNone`. | ||
| 75 | BufferRef<uint> parents; | ||
| 76 | /// The locals and worlds, by slot. | ||
| 77 | BufferRef<float4> locals; | ||
| 78 | BufferRef<float4> worlds; | ||
| 79 | uint count; | ||
| 80 | }; | ||
| 81 | |||
| 82 | #ifdef __cplusplus | ||
| 83 | } // namespace ernie::shaders | ||
| 84 | #else | ||
| 85 | |||
| 86 | /// A slot's world transform relative to the render origin, as float rows. | ||
| 87 | Affine LoadWorld(BufferRef<float4> worlds, uint slot) { | ||
| 88 | return Round(LoadDouble(worlds, slot)); | ||
| 89 | } | ||
| 90 | |||
| 91 | static const uint3 kAxes[6] = { uint3(0, 1, 2), uint3(0, 2, 1), uint3(1, 0, 2), | ||
| 92 | uint3(1, 2, 0), uint3(2, 0, 1), uint3(2, 1, 0) }; | ||
| 93 | |||
| 94 | /// A right-handed turn about one axis, as the host's AxisRotation builds it. | ||
| 95 | Affine Turn(uint axis, float degrees) { | ||
| 96 | float c = cos(radians(degrees)); | ||
| 97 | float s = sin(radians(degrees)); | ||
| 98 | uint a = (axis + 1) % 3; | ||
| 99 | uint b = (axis + 2) % 3; | ||
| 100 | Affine m = Identity(); | ||
| 101 | m.rows[a][a] = c; | ||
| 102 | m.rows[a][b] = -s; | ||
| 103 | m.rows[b][a] = s; | ||
| 104 | m.rows[b][b] = c; | ||
| 105 | return m; | ||
| 106 | } | ||
| 107 | |||
| 108 | Affine Inverse(Affine m) { | ||
| 109 | float3 a = m.rows[0].xyz; | ||
| 110 | float3 b = m.rows[1].xyz; | ||
| 111 | float3 c = m.rows[2].xyz; | ||
| 112 | float3 r0 = cross(b, c); | ||
| 113 | float3 r1 = cross(c, a); | ||
| 114 | float3 r2 = cross(a, b); | ||
| 115 | float invDet = 1 / dot(a, r0); | ||
| 116 | // The inverse's columns are the cofactor rows over the determinant. | ||
| 117 | float3x3 inv = transpose(float3x3(r0, r1, r2)) * invDet; | ||
| 118 | float3 t = float3(m.rows[0].w, m.rows[1].w, m.rows[2].w); | ||
| 119 | float3 back = -mul(inv, t); | ||
| 120 | Affine result; | ||
| 121 | for (uint r = 0; r < 3; ++r) { | ||
| 122 | result.rows[r] = float4(inv[r], back[r]); | ||
| 123 | } | ||
| 124 | return result; | ||
| 125 | } | ||
| 126 | |||
| 127 | /// A bound op's matrix from the floats it reads. | ||
| 128 | Affine OpMatrix(Op op) { | ||
| 129 | Affine m = Identity(); | ||
| 130 | BufferRef<float> v = op.source; | ||
| 131 | uint floats = (op.flags >> kFloatsShift) & 0xff; | ||
| 132 | if (op.kind == kTranslate) { | ||
| 133 | m.rows[0].w = v[0]; | ||
| 134 | m.rows[1].w = v[1]; | ||
| 135 | m.rows[2].w = v[2]; | ||
| 136 | } else if (op.kind == kScale) { | ||
| 137 | float3 s = floats == 1 ? float3(v[0]) : float3(v[0], v[1], v[2]); | ||
| 138 | m.rows[0].x = s.x; | ||
| 139 | m.rows[1].y = s.y; | ||
| 140 | m.rows[2].z = s.z; | ||
| 141 | } else if (op.kind < kRotateXYZ) { | ||
| 142 | m = Turn(op.kind - kRotateX, v[0]); | ||
| 143 | } else if (op.kind < kOrient) { | ||
| 144 | // The order's first axis turns first: R = Rk * Rj * Ri, each by its own axis' angle. | ||
| 145 | uint3 axes = kAxes[op.kind - kRotateXYZ]; | ||
| 146 | m = Mul(Mul(Turn(axes.z, v[axes.z]), Turn(axes.y, v[axes.y])), | ||
| 147 | Turn(axes.x, v[axes.x])); | ||
| 148 | } else if (op.kind == kOrient) { | ||
| 149 | float4 q = normalize(float4(v[0], v[1], v[2], v[3])); | ||
| 150 | m.rows[0] = float4(1 - 2 * (q.y * q.y + q.z * q.z), 2 * (q.x * q.y - q.z * q.w), | ||
| 151 | 2 * (q.x * q.z + q.y * q.w), 0); | ||
| 152 | m.rows[1] = float4(2 * (q.x * q.y + q.z * q.w), 1 - 2 * (q.x * q.x + q.z * q.z), | ||
| 153 | 2 * (q.y * q.z - q.x * q.w), 0); | ||
| 154 | m.rows[2] = float4(2 * (q.x * q.z - q.y * q.w), 2 * (q.y * q.z + q.x * q.w), | ||
| 155 | 1 - 2 * (q.x * q.x + q.y * q.y), 0); | ||
| 156 | } else { | ||
| 157 | // Column-major, as the scene's matrices are. | ||
| 158 | for (uint r = 0; r < 3; ++r) { | ||
| 159 | m.rows[r] = float4(v[r], v[4 + r], v[8 + r], v[12 + r]); | ||
| 160 | } | ||
| 161 | } | ||
| 162 | return (op.flags & kInvert) != 0 ? Inverse(m) : m; | ||
| 163 | } | ||
| 164 | |||
| 165 | /// An op's matrix: a folded constant exactly as the host split it, a bound op from its floats. | ||
| 166 | DoubleAffine OpDoubleMatrix(Op op) { | ||
| 167 | if (op.kind != kConstant) { | ||
| 168 | return Promote(OpMatrix(op)); | ||
| 169 | } | ||
| 170 | DoubleAffine m; | ||
| 171 | for (uint r = 0; r < 3; ++r) { | ||
| 172 | m.rows[r].hi = op.constant.high[r]; | ||
| 173 | m.rows[r].lo = op.constant.low[r]; | ||
| 174 | } | ||
| 175 | return m; | ||
| 176 | } | ||
| 177 | |||
| 178 | /// One thread a programmed node, its program at its place in the list. | ||
| 179 | [shader("compute")] | ||
| 180 | [numthreads(64, 1, 1)] | ||
| 181 | void Local(uint3 id : SV_DispatchThreadID, uniform LocalParameters parameters) { | ||
| 182 | uint at = id.x; | ||
| 183 | if (at >= parameters.count) { | ||
| 184 | return; | ||
| 185 | } | ||
| 186 | DoubleAffine local = Promote(Identity()); | ||
| 187 | for (uint k = 0; k < parameters.lengths[at]; ++k) { | ||
| 188 | local = Mul(local, OpDoubleMatrix(parameters.programs[at * kMaxOps + k])); | ||
| 189 | } | ||
| 190 | StoreDouble(parameters.locals, parameters.slots[at], local); | ||
| 191 | } | ||
| 192 | |||
| 193 | /// One level of the graph: each node's world from its parent's and its local. | ||
| 194 | [shader("compute")] | ||
| 195 | [numthreads(64, 1, 1)] | ||
| 196 | void Compose(uint3 id : SV_DispatchThreadID, uniform ComposeParameters parameters) { | ||
| 197 | if (id.x >= parameters.count) { | ||
| 198 | return; | ||
| 199 | } | ||
| 200 | uint slot = parameters.slots[id.x]; | ||
| 201 | uint parent = parameters.parents[slot]; | ||
| 202 | DoubleAffine local = LoadDouble(parameters.locals, slot); | ||
| 203 | StoreDouble(parameters.worlds, slot, | ||
| 204 | parent == kNone ? local : Mul(LoadDouble(parameters.worlds, parent), local)); | ||
| 205 | } | ||
| 206 | |||
| 207 | #endif | ||
| 208 |