Vixen
dd8b0a81
csharp
public static class MorphKernel

Applying blend-shape weights to a vertex stream — the arithmetic, written once.

Read the guide page for this →

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 EntryWords

    How 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