public static class MorphKernelApplying blend-shape weights to a vertex stream — the arithmetic, written once.
Remarks
The CPU half of Raven/Library/Pipeline/MorphScatter.rvn, and the reason both exist. MeshletPages says it best about its own pair: two shaders that agree with a third party agree with each other; two that are only ever compared with each other agree on whatever they both get wrong. So the scatter is written here as ordinary C#, the compute kernel is a transliteration of it, and MorphScatterDeviceTests holds the two to the same numbers on a device.
What it does is one line of arithmetic and one decision. The line is v += w·Δ per entry per active target. The decision — the one that makes this a pre-pass rather than a loop in every vertex stage — is doc 33 § D4's: the morphed vertices go in a buffer, and that buffer is what the shading pass, the shadow pass, the velocity pass and the depth pre-pass all read. A vertex shader that morphed inline would do the work four times and, worse, could disagree with itself between passes — which is the bug that shows up as a face whose shadow does not match it.
⚠ Nothing here renormalises the morphed normal, and that is deliberate twice over. Once for correctness: a target may cancel a normal exactly — Δn = −n at full weight is a legitimate authored shape — and normalising a zero vector is precisely the case Vector3.Normalize answers with infinities, because its tolerance is an absolute 1e-6 and not a relative one. Once for parity: rsqrt and 1/sqrt are not the same function, so a normalise here would be a divergence between the two processors that has nothing to do with morphing. The consumer already does it safely — ForwardPlus's fragment stage calls Math.SafeNormalize on the interpolated normal, whose tolerance is 1e-4 and whose degenerate answer is zero rather than a NaN.
⚠ One dispatch per active target, not one per instance, which is where the implementation and § D4's sketch differ and it is worth saying why. Two targets may move the same vertex — that is what a corrective is — so a single dispatch over the concatenated entries would have two invocations read-modify-writing one vertex, and the answer would depend on which of them won. Vixen's Raven has no float atomic to fix that with. A target's own indices are distinct by construction, so a dispatch per target with a barrier between is race-free and costs one dispatch per active shape — twenty for a face, not one per instance. The single-dispatch form needs the deltas re-indexed by vertex at import time, which is a format change and is named as owed rather than guessed at here.
Fields and properties (1)
public const int EntryWordsHow many 32-bit words one entry occupies in the buffer the kernel reads.
Methods (5)
public static void Apply(ReadOnlySpan<SurfaceVertex> source, IReadOnlyList<MorphTargetData> targets, ReadOnlySpan<float> weights, Span<SurfaceVertex> into)Morphs a vertex stream by a set of weights.
public static void Accumulate(MorphTargetData target, float weight, Span<SurfaceVertex> into)Adds one target's deltas, weighted, into a vertex stream already holding the base.
public static uint[] Pack(MorphTargetData target)One target's entries in the layout MorphScatter.rvn reads.
public static void Pack(MorphTargetData target, Span<uint> into)Writes one target's entries into a span, in the layout MorphScatter.rvn reads.
public static float Step(float scale)What one quantised unit of a scale is worth, which is the kernel's multiplier.
Used by (5)
- MorphIndexVixen.Rendering
- MorphIndexTestsVixen.Rendering.Tests
- MorphRenderFeatureVixen.Rendering
- MorphScatterDeviceTestsVixen.Graphics.Golden.Tests
- MorphTargetTestsVixen.Rendering.Tests