Skip to content

SDEF Skinning

CandidumDevTeam edited this page Jul 3, 2026 · 2 revisions

SDEF Skinning

SDEF (Spherical Deform) is PMX's fourth weight type, alongside BDEF1, BDEF2 and BDEF4. Where BDEF blends bones linearly, SDEF blends them spherically around a per-vertex centre so that skin bends around a joint instead of collapsing into it. It is what keeps shoulders, elbows, wrists and the base of the neck from pinching when a limb rotates hard. MMD applies it to a small but visible fraction of a model's vertices, and getting it right is the difference between a clean elbow and a candy-wrapper crease.

UMT reproduces SDEF exactly, and it does so through a full-mesh skinning pass that runs the spherical blend every frame against the live bone poses. That pass has two implementations that compute the same result: a GPU compute shader and a CPU Burst job. Which one runs is chosen per model by MMDTransformManager.sdefSkinningMode (GPU by default), and a missing compute shader forces the CPU path. There is no CPU-vs-GPU decision based on hardware capability — the mode is a setting, not a probe — so the "platform requirement" story on the old version of this page no longer applies.

What SDEF stores

Each SDEF vertex carries, in addition to its two bone indices b0, b1 and the first bone weight w0 (with w1 = 1 − w0):

  • C : the skinning centre, the pivot the vertex rotates around.
  • R0, R1 : two reference points that bias the centre toward each bone.

PMXReader converts all three to Unity space as it parses (the 180° Y rotation and the 0.08 unit scale, exactly as for any other position, see MMD Formats and Internals). By the time the mesh builder sees them they are already in Unity units, and nothing scales them again.

The maths

The formula has two halves that are averaged: a translation estimate that moves the centre linearly with the bones, and a rotation estimate that swings the vertex around that centre with a slerped bone rotation. Splitting the vertex's motion this way is what produces the rounded, volume-preserving bend.

Import-time correction

The raw R0/R1 do not blend back to the centre on their own, so UMT precomputes a corrected pair once at import (MMDSDEFMath.CorrectSDEF, called from PMXMeshBuilder). With w1 = 1 − w0:

Rblend = R0·w0 + R1·w1
R0'    = C + R0 − Rblend
R1'    = C + R1 − Rblend

The correction guarantees that R0'·w0 + R1'·w1 = C, i.e. under the rest pose the reference points collapse exactly onto the centre, so an un-posed SDEF vertex sits precisely where it started. The corrected R0', R1' and the centre C are baked into the per-vertex SDEFVertexData uploaded to the skinner; the raw R0/R1 are never sent.

Per-frame deform

Let M0, M1 be the two bones' skinning matrices for this frame (each is boneWorldMatrix · bindpose, rebased into the renderer's root-bone space) and q0, q1 their rotation quaternions. For a bind-pose vertex p with normal n:

Translation half : move the centre linearly with each bone, then blend:

p0 = M0 · C                       (C transformed by bone 0)
p1 = M1 · C                       (C transformed by bone 1)
t  = (p0 − C)·w0 + (p1 − C)·w1    (blended displacement of the centre)
Ptrans = C + t

Rotation half : transform the corrected reference points and blend:

Prot = (M0 · R0')·w0 + (M1 · R1')·w1

Averaged centre : the SDEF pivot for this frame:

Pc = (Ptrans + Prot) · 0.5

Spherical bone rotation : slerp the two bone rotations by the second weight, so w1 = 0 gives all of bone 0 and w1 = 1 gives all of bone 1:

q = slerp(q0, q1, w1)

Final vertex : rotate the vertex about the original centre C, then place it at the averaged pivot Pc:

p_out = q · (p − C) + Pc
n_out = q · n

This is the standard MMD/Saba SDEF form: rotation is taken about the vertex's own centre C, while the pivot it lands on, Pc, is the average of the linear-translation and rotation estimates above. The slerp uses a shortest-path sign flip and an nlerp fallback when q0 and q1 are nearly parallel, to avoid dividing by a near-zero sin θ.

Contrast this with the BDEF (linear blend) path the same pass runs for every non-SDEF vertex, which is the familiar weighted sum of transformed positions:

p_out = Σ wi · (Mi · p)
n_out = Σ wi · (qi · n)

The linear form moves each vertex straight toward the bone; the SDEF form curves it around the joint. That curvature is the whole point. The two forms live side by side in one pass: a per-vertex deform-type flag (SDEFVertexData.sdefC.w, 0 = linear, 1 = SDEF) selects which is applied, so a mesh that mixes SDEF and BDEF vertices is handled correctly.

How it runs in the pipeline

SDEF is a full-mesh skinning pass, not a CPU pre-bake of a static pose, because the spherical blend has to be re-evaluated every frame against the live bone poses. The same pass also folds in vertex-morph blend shapes, in bind-pose space before skinning, so morphs and SDEF coexist correctly.

  • Import. PMXMeshBuilder detects any SDEF vertex in a generated mesh, marks the mesh hasSDEF, and fills a per-vertex SDEFVertexData buffer (corrected R0'/R1', centre, bone indices and weights, and the deform-type flag). The mesh's vertex buffer is flagged for raw GPU access so the compute pass can read it.

  • Build. MMDSDEFSkinnerBuilder attaches one MMDSDEFSkinner component to each SDEF renderer, wiring the per-vertex data, the mesh bindposes, and a compact per-mesh morph table. It also sets updateWhenOffscreen on the renderer, because a mesh whose vertices are rewritten by the skinner can't compute its own bounds and would otherwise be culled wrongly.

  • Runtime. The owning MMDTransformManager drives the skinners once per frame, after the bone solve. The two modes are driven from different points in the frame:

    • GPU mode dispatches from the render-pipeline pre-render callback (both the SRP RenderPipelineManager.beginCameraRendering event and the built-in Camera.onPreRender are subscribed; only the active pipeline fires, and a frame guard keeps it to one dispatch per frame). The compute shader reads the bind-pose source buffer, applies morphs, applies skinning, and writes the result straight into the SkinnedMeshRenderer's output vertex buffer, overwriting Unity's linear-blended result. Dispatching at camera-render time is what lets the compute overwrite win over Unity's own skinning.
    • CPU mode schedules the equivalent MMDSDEFSkinJob (a Burst IJobParallelFor) from LateUpdate, completes it, and uploads the result into a runtime pass-through mesh on the renderer. CPU mode runs in play mode only.

    Either way the bone matrices are sourced from the live SkinnedMeshRenderer.bones, so SDEF tracks whatever poses the skeleton — the MMD solver, an Animator, or manual posing alike.

Choosing and configuring the mode

The mode lives on the transform manager as sdefSkinningMode (SDEFSkinningMode.GPU or .CPU), with doSDEFSkinning as a master on/off, and sdefComputeShader holding the compute kernel for the GPU path. The rules are simple:

  • GPU is the default. The compute shader ships on the UMTResources asset and is wired onto the transform manager at import.
  • A null compute shader forces CPU. If the resources asset is stripped or misconfigured and no SDEF compute shader is present, import raises the warning "SDEF meshes were imported but no SDEF compute shader is assigned on UMTResources; falling back to CPU SDEF skinning." and sets the model's mode to CPU.
  • You can set the mode yourself on the MMDTransformManager if you want to force one path — for example CPU on a target where you don't want to rely on the compute overwrite path, keeping in mind CPU SDEF is play-mode only.

There is no hardware-capability check anywhere in the package: the mode is exactly what sdefSkinningMode says (adjusted only by the null-shader fallback above). Both paths produce the same deform; the choice is about where the work runs, not whether SDEF is faithful.

Debugging notes

  • If a model you know uses SDEF shows pinched joints, first confirm the SDEF pass is actually running: check that doSDEFSkinning is on, that the renderer has an MMDSDEFSkinner, and — for GPU mode — that sdefComputeShader is assigned. If import warned about a missing compute shader, the model is on the CPU path, which only runs in play mode, so an edit-mode preview will look un-deformed.
  • Any vertex that never resolves to SDEF (its source weights weren't SDEF) simply runs the linear path in the same pass; that is expected for the BDEF portion of a mixed mesh.
  • SDEFVertexData's layout is byte-for-byte shared with the struct in MMDSDEFSkinning.compute (every field is 16 bytes), and the CPU job reads the same struct. Changing one side without the others corrupts the buffer, the same discipline the physics marshalling structs need.

See MMD Formats and Internals for where SDEF sits among the other weight types, and the Programming Reference for the surrounding import API.

Clone this wiki locally