Add fur-map support, skinned driver, and world-down curvature to fur shell

FurShell now reads an RGBA fur map (RGB = per-texel strand colour,
A = fur length) so the standalone look can be driven per-texel. Alpha 0
clips the whole shell stack (root included) so a painted body shows
through; _SolidRoot keeps the opaque base shell only for the full-coverage
prototype. White map default keeps the standalone prototype unchanged.

FurShellRenderer drops the MeshFilter requirement and resolves its mesh
per frame: on a SkinnedMeshRenderer it borrows the SkinnedColliderBaker's
per-frame baked pose so the fur follows the animation with no extra bake.

Curvature/gravity now bends strands toward WORLD down (converted into
object space via unity_WorldToObject) instead of object-local down, so a
rotated player mesh droops downward correctly. Identity rotation is
unchanged, so the test sphere is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-07-03 20:47:15 +02:00
1 parent bf4297a3b1
commit 43791abc79
2 files changed
+84 -17

No files matched your search

+38 -6
View File
@@ -35,6 +35,15 @@ Shader "Custom/FurShell"
_RimPower("Rim / Backlight Power", Range(0.5, 8.0)) = 3.0
_RimStrength("Rim / Backlight Strength", Range(0.0, 2.0)) = 0.4
[Header(Fur Map)]
// RGB = per-texel strand colour, A = fur length (0 = bald here, 1 = full length). White default
// means "full-length fur everywhere" so the standalone prototype behaves as before with no map.
_FurMap("Fur Map (RGB color, A length)", 2D) = "white" {}
_FurThreshold("Fur Cutoff (alpha)", Range(0.0, 1.0)) = 0.0
// Opaque base shell. On = solid root (full-coverage prototype); Off = the root also clips to the
// map so the painted body shows between strands (avatar integration).
[ToggleUI] _SolidRoot("Solid Root", Float) = 1
[Header(Wind)]
_WindStrength("Wind Strength", Range(0.0, 1.0)) = 0.1
_WindSpeed("Wind Speed", Range(0.0, 10.0)) = 2.0
@@ -91,6 +100,8 @@ Shader "Custom/FurShell"
half _RootAO;
half _RimPower;
half _RimStrength;
float _FurThreshold;
float _SolidRoot;
float _WindStrength;
float _WindSpeed;
float _WindFrequency;
@@ -99,6 +110,8 @@ Shader "Custom/FurShell"
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
TEXTURE2D(_FurMap);
SAMPLER(sampler_FurMap);
struct Attributes
{
@@ -141,7 +154,12 @@ Shader "Custom/FurShell"
// extrude along the normal; bend the tips down with h^2 for a gravity/curl droop
float3 posOS = input.positionOS.xyz + input.normalOS * (_ShellLength * h);
posOS += float3(0.0, -1.0, 0.0) * (_Curvature * _ShellLength * h * h);
// gravity pulls toward WORLD down, so strands hang downward no matter how the mesh is
// oriented - a rotated player mesh would otherwise droop along its own local down. Convert
// world down into object space (the extrusion space) so the droop magnitude stays consistent
// with _ShellLength under the object-to-world transform.
float3 downOS = normalize(mul((float3x3)unity_WorldToObject, float3(0.0, -1.0, 0.0)));
posOS += downOS * (_Curvature * _ShellLength * h * h);
// wind sway: nudge each shell along the wind direction (object XZ) by a time-varying
// amount. The phase is offset by object-space position so a gust rolls across the surface,
@@ -171,6 +189,11 @@ Shader "Custom/FurShell"
float3 N = normalize(input.normalWS);
float3 V = GetWorldSpaceNormalizeViewDir(input.positionWS);
// fur map: RGB = strand colour, A = fur length. White default (no map) => full-length fur
// everywhere, so the standalone prototype is unchanged.
half4 furSample = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv);
float furLen = furSample.a;
// --- strand mask: one strand per cell, random length, radially tapered toward the tip ---
float2 g = input.uv * _Density;
float2 cell = floor(g);
@@ -230,14 +253,23 @@ Shader "Custom/FurShell"
float taperedR = pow(saturate(1.0 - h), _Taper * 0.5);
float radius = _Thickness * max(taperedR, _TipFill);
// the root shell (h==0) stays fully solid so nothing shows through to the surface
half alpha = 1.0h;
if (h > 0.0)
alpha = (dist < radius && h <= strandLen) ? 1.0h : 0.0h;
// The fur map's alpha caps this strand's height: 0 => bald, so the whole shell stack
// (root included) clips and the body shows through; 1 => the strand reaches its full
// random length. _SolidRoot keeps the base shell opaque for the full-coverage prototype;
// on the painted avatar it is turned OFF so the camo body stays visible between strands
// instead of being sealed under a solid root (which would also Z-fight the body).
float effectiveLen = strandLen * furLen;
half alpha;
if (furLen <= _FurThreshold)
alpha = 0.0h;
else if (h <= 0.0 && _SolidRoot > 0.5)
alpha = 1.0h;
else
alpha = (dist < radius && h <= effectiveLen) ? 1.0h : 0.0h;
clip(alpha - 0.5h);
// --- shading (lightweight URP) ---
half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb;
half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * furSample.rgb * _BaseColor.rgb;
albedo = lerp(albedo, _TipColor.rgb, h);
half ao = lerp(1.0h - _RootAO, 1.0h, h); // roots darker (fake self-occlusion)
albedo *= ao;