Fur: Albedo-gekoppelte Länge, Toggle-Gate, Soft-Edge, World-Gravity, Pipette & FP-Hide
- Fur-Map trägt nur noch R = Länge (linearer Datenkanal); Farbe kommt per-texel aus der Live-Albedo-RT (an _BaseMap des Shell-Shaders gebunden) - Toggle_Fur als echtes Gate (IsChannelActive = fur), Default aus, furLength 0.5 - Weicher Alpha-Übergang (radial/tip/length coverage) statt Hard-Clip; Transparent- Pass mit Depth-Bias (Offset -1,-1) gegen Body-Durchflimmern - Root dunkler / Tip heller aus gemalter Farbe abgeleitet (kein Fix-Tint) - Gravity/Curl in World-Space nach Transform (unabhängig von Mesh-Rotation) - Pipette aktiviert Fur-Toggle immer; liest R-Kanal als Länge - First-Person blendet FurShellRenderer mit aus - Netzwerk (FlagFur), Undo, Persistence (fur.png R=Länge) mitgezogen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
7536e8ec8b
commit
1b783a39be
15 files changed
+254
-306
No files matched your search
@@ -28,18 +28,28 @@ Shader "Custom/FurShell"
|
||||
_SilhouetteStretch("Silhouette Strand Stretch", Range(0.0, 5.0)) = 2.0
|
||||
|
||||
[Header(Fur Shading)]
|
||||
[MainTexture] _BaseMap("Albedo", 2D) = "white" {}
|
||||
[MainColor] _BaseColor("Root Tint", Color) = (0.42, 0.32, 0.20, 1)
|
||||
_TipColor("Tip Tint", Color) = (0.62, 0.52, 0.36, 1)
|
||||
_RootAO("Root Darkening (fake AO)", Range(0.0, 1.0)) = 0.45
|
||||
// The strand colour: the painted ALBEDO map (the camo under the fur). FurShellRenderer binds the
|
||||
// sibling painter's live albedo RT here, so each strand wears the colour beneath it.
|
||||
[MainTexture] _BaseMap("Albedo (strand colour)", 2D) = "white" {}
|
||||
// Overall tint multiplied onto the painted fur colour; white leaves the painted colour untouched.
|
||||
[MainColor] _BaseColor("Fur Tint", Color) = (1, 1, 1, 1)
|
||||
// Root/tip are two shades of the SAME painted colour: roots a touch darker, tips a touch lighter.
|
||||
_RootAO("Root Darkening", Range(0.0, 1.0)) = 0.3
|
||||
_TipShade("Tip Lightening", Range(0.0, 1.0)) = 0.2
|
||||
_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" {}
|
||||
// R = fur length (0 = bald here, 1 = full length); G/B/A unused/reserved. The strand COLOUR now
|
||||
// comes from the albedo map (_BaseMap), so fur wears the painted camo. White default = full-length
|
||||
// fur everywhere, so the standalone prototype (no map) is unchanged.
|
||||
_FurMap("Fur Map (R = length)", 2D) = "white" {}
|
||||
_FurThreshold("Fur Cutoff (alpha)", Range(0.0, 1.0)) = 0.0
|
||||
// Low painted alpha => low opacity, so a lightly-painted texel fades in as faint short fuzz
|
||||
// instead of full-opacity fur with a hard border. Larger = softer/longer fade.
|
||||
_FurSoftness("Edge Softness (length fade)", Range(0.01, 1.0)) = 0.5
|
||||
// Per-strand anti-aliasing: soft strand silhouette + tip instead of hard alpha-clipped discs.
|
||||
_StrandSoftness("Strand Edge Softness", Range(0.0, 1.0)) = 0.3
|
||||
// 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
|
||||
@@ -55,9 +65,9 @@ Shader "Custom/FurShell"
|
||||
{
|
||||
Tags
|
||||
{
|
||||
"RenderType" = "Opaque"
|
||||
"RenderType" = "Transparent"
|
||||
"RenderPipeline" = "UniversalPipeline"
|
||||
"Queue" = "Geometry"
|
||||
"Queue" = "Transparent"
|
||||
}
|
||||
LOD 200
|
||||
|
||||
@@ -67,8 +77,17 @@ Shader "Custom/FurShell"
|
||||
Tags { "LightMode" = "UniversalForward" }
|
||||
|
||||
Cull Back
|
||||
ZWrite On
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
ZWrite Off
|
||||
ZTest LEqual
|
||||
// Depth bias toward the camera so the fur wins the depth test against the body. The fur
|
||||
// extrudes a CPU-baked mesh while the body uses GPU skinning, so their surfaces are not
|
||||
// bit-identical: where the body pokes a hair in front of a shell layer, that shell fragment
|
||||
// fails ZTest (ZWrite Off => no depth write to win the tie) and the body flickers through as
|
||||
// white speckles. Offset shifts the fragment's tested depth (respected even with ZWrite Off),
|
||||
// slope-scaled + a constant unit, so the fur reliably passes without moving geometry (no
|
||||
// visible root gap). Small enough not to punch through objects genuinely in front.
|
||||
Offset -1, -1
|
||||
|
||||
HLSLPROGRAM
|
||||
#pragma target 3.5
|
||||
@@ -87,7 +106,6 @@ Shader "Custom/FurShell"
|
||||
CBUFFER_START(UnityPerMaterial)
|
||||
float4 _BaseMap_ST;
|
||||
half4 _BaseColor;
|
||||
half4 _TipColor;
|
||||
float _ShellCount;
|
||||
float _ShellLength;
|
||||
float _Density;
|
||||
@@ -98,9 +116,12 @@ Shader "Custom/FurShell"
|
||||
float _DirRandomness;
|
||||
float _SilhouetteStretch;
|
||||
half _RootAO;
|
||||
half _TipShade;
|
||||
half _RimPower;
|
||||
half _RimStrength;
|
||||
float _FurThreshold;
|
||||
float _FurSoftness;
|
||||
float _StrandSoftness;
|
||||
float _SolidRoot;
|
||||
float _WindStrength;
|
||||
float _WindSpeed;
|
||||
@@ -152,14 +173,13 @@ Shader "Custom/FurShell"
|
||||
float count = max(_ShellCount, 1.0);
|
||||
float h = (count <= 1.0) ? 0.0 : (float)shellIndex / (count - 1.0);
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
// extrude along the normal; bend the tips down with h^2 for a gravity/curl droop.
|
||||
// The (h + 0.02) lifts the whole stack a hair off the surface: the root (h=0) would else
|
||||
// sit exactly on the opaque body and z-fight it - the fur draws a CPU-baked mesh while the
|
||||
// body uses GPU skinning, so their depths are not bit-identical, and now that fur is
|
||||
// transparent (ZWrite Off) it can't rely on a depth write to win the tie. 0.02 of the shell
|
||||
// length is invisible but separates the root cleanly.
|
||||
float3 posOS = input.positionOS.xyz + input.normalOS * (_ShellLength * (h + 0.02));
|
||||
|
||||
// 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,11 +191,20 @@ Shader "Custom/FurShell"
|
||||
float phase = _Time.y * _WindSpeed + (input.positionOS.x + input.positionOS.z) * _WindFrequency;
|
||||
posOS.xz += wdir * (_WindStrength * _ShellLength * h * h * sin(phase));
|
||||
|
||||
VertexPositionInputs vp = GetVertexPositionInputs(posOS);
|
||||
VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS);
|
||||
// Gravity/curl bends toward WORLD down. Applied AFTER moving to world space and using ONLY
|
||||
// unity_ObjectToWorld (positions/normals already prove that matrix correct). The previous
|
||||
// object-space approach relied on unity_WorldToObject, which is NOT reliably populated in the
|
||||
// RenderMeshInstanced path - when it falls back to identity the droop follows the mesh's own
|
||||
// local down, so on the rotated player the fur hung sideways instead of toward true down.
|
||||
// objScale keeps the droop magnitude consistent with the normal extrusion (object units).
|
||||
float3 positionWS = TransformObjectToWorld(posOS);
|
||||
float objScale = length(mul((float3x3)unity_ObjectToWorld, float3(0.0, 1.0, 0.0)));
|
||||
positionWS.y -= _Curvature * _ShellLength * h * h * objScale;
|
||||
|
||||
output.positionCS = vp.positionCS;
|
||||
output.positionWS = vp.positionWS;
|
||||
VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS);
|
||||
|
||||
output.positionCS = TransformWorldToHClip(positionWS);
|
||||
output.positionWS = positionWS;
|
||||
output.normalWS = vn.normalWS;
|
||||
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
|
||||
output.shellH = h;
|
||||
@@ -189,10 +218,10 @@ 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;
|
||||
// fur map: R = fur length (G/B/A unused). White default (no map) => full-length fur
|
||||
// everywhere, so the standalone prototype is unchanged. The strand colour comes from the
|
||||
// albedo map (_BaseMap) below, not from here.
|
||||
float furLen = SAMPLE_TEXTURE2D(_FurMap, sampler_FurMap, input.uv).r;
|
||||
|
||||
// --- strand mask: one strand per cell, random length, radially tapered toward the tip ---
|
||||
float2 g = input.uv * _Density;
|
||||
@@ -253,26 +282,42 @@ Shader "Custom/FurShell"
|
||||
float taperedR = pow(saturate(1.0 - h), _Taper * 0.5);
|
||||
float radius = _Thickness * max(taperedR, _TipFill);
|
||||
|
||||
// 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).
|
||||
// --- soft coverage (replaces the old binary alpha-clip) so the fur fades IN with alpha
|
||||
// instead of a hard on/off edge. Three factors multiply into the output alpha:
|
||||
// radialCov : soft, anti-aliased strand edge instead of a hard disc.
|
||||
// heightCov : each strand fades out toward its own random tip (_TipFill caps the height).
|
||||
// lenOpacity : LOW painted alpha => LOW opacity. A lightly-painted texel reads as faint,
|
||||
// short fuzz that blends over the body -> the soft transition asked for,
|
||||
// instead of full-opacity fur with a hard border. The fur map's alpha still
|
||||
// also caps the strand HEIGHT (effectiveLen), so low alpha is short AND faint.
|
||||
// The mesh is drawn on the Transparent queue and the shells are submitted root->tip, i.e.
|
||||
// back-to-front on front faces, so these alphas composite correctly over the camo 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);
|
||||
float radialCov = 1.0 - smoothstep(radius - max(radius * _StrandSoftness, 1e-4), radius, dist);
|
||||
float heightCov = 1.0 - smoothstep(effectiveLen * (1.0 - _StrandSoftness), effectiveLen + 1e-4, h);
|
||||
float lenOpacity = saturate((furLen - _FurThreshold) / max(_FurSoftness, 1e-4));
|
||||
|
||||
half alpha = (half)(radialCov * heightCov * lenOpacity);
|
||||
|
||||
// _SolidRoot keeps the base shell fully covering (full-coverage prototype), but still honours
|
||||
// the painted length so the boundary keeps its soft fade instead of a hard opaque ring. On
|
||||
// the painted avatar it is turned OFF so the root also thins to the strands and the camo body
|
||||
// stays visible between them.
|
||||
if (_SolidRoot > 0.5 && h <= 0.0)
|
||||
alpha = (half)lenOpacity;
|
||||
|
||||
if (furLen <= _FurThreshold) alpha = 0.0h;
|
||||
clip(alpha - 0.003h); // drop fully-transparent fragments (perf + avoids stray blend)
|
||||
|
||||
// --- shading (lightweight URP) ---
|
||||
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;
|
||||
// base colour is the PAINTED ALBEDO (the camo under the fur), sampled per-texel so each
|
||||
// strand wears the colour beneath it, times the global tint (white by default). The fur map
|
||||
// no longer carries colour - only length.
|
||||
half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv).rgb * _BaseColor.rgb;
|
||||
// two shades of that one colour: roots a touch darker (fake self-occlusion), tips a touch
|
||||
// lighter - a subtle root->tip gradient rather than two independent colours.
|
||||
half ao = lerp(1.0h - _RootAO, 1.0h, h);
|
||||
albedo *= ao * lerp(1.0h, 1.0h + _TipShade, h);
|
||||
|
||||
float4 shadowCoord = TransformWorldToShadowCoord(input.positionWS);
|
||||
Light mainLight = GetMainLight(shadowCoord);
|
||||
@@ -296,7 +341,7 @@ Shader "Custom/FurShell"
|
||||
half rim = pow(saturate(1.0h - saturate(dot(N, V))), _RimPower) * _RimStrength * h;
|
||||
color += rim * mainLight.color * albedo;
|
||||
|
||||
return half4(color, 1.0h);
|
||||
return half4(color, alpha);
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user