FurShell: scale-aware length/curvature + X-ray glow mirror
The fur is drawn from a CPU-baked unit-scale mesh whose instance matrix carries the avatar's world scale, so the extrusion scaled fully 1:1 with the player (hider 0.5 / hunter 1.5). That read as too-short fur on the small hider and too-long on the big hunter, and the curvature droop was sized off the raw unity_ObjectToWorld which doesn't carry the per-instance scale in the RenderMeshInstanced path. - FurShellRenderer pushes the avatar's lossyScale (_FurObjScale) plus a new Fur Scale Influence (0..1, default 0.5) that blends the fur SIZE between fully proportional (1) and a constant world length (0). The shader pre-divides the object-space displacement so the net world length follows effScale; curl droop and wind use the same effScale, keeping the curl SHAPE invariant across avatar sizes and influence settings. - The body's see-through X-ray reveal (forward half) is mirrored onto the visible fur so strands glow WITH the reveal instead of hiding it under opaque fur. FurShellRenderer pulls the live colour/pulse/flash from the sibling MeshPainter (new getters), in phase with the body. A new X-Ray Fur Strength (default 0.2) tames the per-shell accumulation, since the body writes the glow once (opaque) but the fur stacks N transparent shells that each composite it. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
d9e4aec927
commit
9dc8f21528
3 files changed
+132
-5
No files matched your search
@@ -41,6 +41,11 @@ namespace Chameleon.Components
|
||||
// direction so the flat shell slices merge into continuous strands at the rim instead of squished
|
||||
// dots. 0 = off (footprint stays circular everywhere).
|
||||
[SerializeField, Range(0f, 5f)] float _silhouetteStretch = 2f;
|
||||
// How much the avatar's world scale drives the fur SIZE. 1 = fully proportional (small avatar => small
|
||||
// fur, like the body). 0 = constant world size (same absolute fur length at any avatar scale). Fully
|
||||
// proportional fur reads as too short on the small hider and too long on the big hunter, so dial this
|
||||
// down to keep the fur closer to a fixed world length; the curl keeps its shape at any setting.
|
||||
[SerializeField, Range(0f, 1f)] float _furScaleInfluence = 0.5f;
|
||||
|
||||
[Header("Shading")]
|
||||
// Optional albedo (strand-colour) override for the standalone prototype; a sibling MeshPainter's
|
||||
@@ -79,6 +84,14 @@ namespace Chameleon.Components
|
||||
// Sway direction in the object's XZ plane; normalized in the shader.
|
||||
[SerializeField] Vector2 _windDirection = new Vector2(1f, 0f);
|
||||
|
||||
[Header("X-Ray Reveal")]
|
||||
// The sibling MeshPainter's body see-through reveal is mirrored onto the fur so the strands glow WITH
|
||||
// the reveal instead of hiding it under opaque fur (colour/pulse are pulled from the painter, in phase
|
||||
// with the body). The fur draws up to _shellCount stacked TRANSPARENT shells, so that additive glow
|
||||
// composites once per overlapping layer and blows out; this scales the per-shell glow down to tame the
|
||||
// stacking so the total reads at roughly the body's intensity. Lower = subtler.
|
||||
[SerializeField, Range(0f, 1f)] float _xrayFurStrength = 0.2f;
|
||||
|
||||
[Header("Rendering")]
|
||||
[SerializeField] ShadowCastingMode _shadowMode = ShadowCastingMode.Off;
|
||||
[SerializeField] bool _receiveShadows = true;
|
||||
@@ -110,6 +123,13 @@ namespace Chameleon.Components
|
||||
static readonly int WindSpeedId = Shader.PropertyToID("_WindSpeed");
|
||||
static readonly int WindFrequencyId = Shader.PropertyToID("_WindFrequency");
|
||||
static readonly int WindDirectionId = Shader.PropertyToID("_WindDirection");
|
||||
static readonly int FurObjScaleId = Shader.PropertyToID("_FurObjScale");
|
||||
static readonly int FurScaleInfluenceId = Shader.PropertyToID("_FurScaleInfluence");
|
||||
static readonly int XRayColorId = Shader.PropertyToID("_XRayColor");
|
||||
static readonly int XRayPulseSpeedId = Shader.PropertyToID("_XRayPulseSpeed");
|
||||
static readonly int XRayPulseStrengthId = Shader.PropertyToID("_XRayPulseStrength");
|
||||
static readonly int XRayPulseShapeId = Shader.PropertyToID("_XRayPulseShape");
|
||||
static readonly int XRayFurStrengthId = Shader.PropertyToID("_XRayFurStrength");
|
||||
|
||||
MeshFilter _meshFilter;
|
||||
SkinnedColliderBaker _baker;
|
||||
@@ -243,6 +263,31 @@ namespace Chameleon.Components
|
||||
_mpb.SetFloat(WindSpeedId, _windSpeed);
|
||||
_mpb.SetFloat(WindFrequencyId, _windFrequency);
|
||||
_mpb.SetVector(WindDirectionId, new Vector4(_windDirection.x, _windDirection.y, 0f, 0f));
|
||||
|
||||
// Avatar world scale (uniform via PlayerScaler) = the scale baked into the instance matrix used
|
||||
// for the shell extrusion, so the shader can scale the curvature droop in lockstep and keep the
|
||||
// curl proportional at any player size (hider 0.5 / hunter 1.5). Read from lossyScale because
|
||||
// the raw object matrix does NOT carry the per-instance scale in the RenderMeshInstanced path.
|
||||
_mpb.SetFloat(FurObjScaleId, transform.lossyScale.y);
|
||||
// Blend the fur size between fully proportional to the avatar (1) and a constant world size (0):
|
||||
// the shader pre-divides the object-space displacement so the net world length follows this.
|
||||
_mpb.SetFloat(FurScaleInfluenceId, _furScaleInfluence);
|
||||
|
||||
// Mirror the sibling painter's live body X-ray reveal onto the fur so the strands glow WITH the
|
||||
// reveal (see-through camo) instead of hiding it under opaque fur. Same colour/pulse/flash the
|
||||
// body uses => in phase. ToRenderColor matches the raw MPB upload to Material.SetColor's gamma
|
||||
// handling (same convention as the fur tint above). Off (alpha 0) when no painter or not revealed
|
||||
// => the shader's additive term is zero, so the fur is unchanged.
|
||||
if (_painter)
|
||||
{
|
||||
_mpb.SetColor(XRayColorId, ToRenderColor(_painter.CurrentXRayColor));
|
||||
_mpb.SetFloat(XRayPulseSpeedId, _painter.XRayPulseSpeed);
|
||||
_mpb.SetFloat(XRayPulseStrengthId, _painter.XRayPulseStrength);
|
||||
_mpb.SetFloat(XRayPulseShapeId, _painter.XRayPulseShape);
|
||||
// Per-shell glow scale: the body writes the reveal once (opaque) but the fur stacks many
|
||||
// transparent shells that each composite it, so scale it down to keep the total near the body.
|
||||
_mpb.SetFloat(XRayFurStrengthId, _xrayFurStrength);
|
||||
}
|
||||
}
|
||||
|
||||
// MaterialPropertyBlock.SetColor uploads raw values (unlike Material.SetColor it does no gamma
|
||||
|
||||
@@ -139,6 +139,31 @@ public partial class MeshPainter : MonoBehaviour
|
||||
if (flash != _xrayFlash) { _xrayFlash = flash; ApplyXRayFlashState(); }
|
||||
}
|
||||
|
||||
/// <summary>The live see-through X-ray glow currently on the body material (rgb = glow colour incl. any
|
||||
/// hit-flash boost, a = eased fade master), for a sibling <see cref="Chameleon.Components.FurShellRenderer"/>
|
||||
/// to mirror onto the fur so the strands glow WITH the reveal instead of hiding it. Fully off
|
||||
/// (alpha 0) until a reveal fades in, or before the material instance is set up.</summary>
|
||||
public Color CurrentXRayColor =>
|
||||
_materialInstance && _materialInstance.HasProperty(XRayColorId)
|
||||
? _materialInstance.GetColor(XRayColorId)
|
||||
: new Color(0f, 0f, 0f, 0f);
|
||||
|
||||
/// <summary>Authored X-ray breathing pulse speed (0 = steady), mirrored onto the fur so its glow breathes
|
||||
/// in phase with the body's forward/overlay halves.</summary>
|
||||
public float XRayPulseSpeed =>
|
||||
_materialInstance && _materialInstance.HasProperty(XRayPulseSpeedId)
|
||||
? _materialInstance.GetFloat(XRayPulseSpeedId) : 0f;
|
||||
|
||||
/// <summary>Live X-ray pulse strength (the hit flash drops this to 0 for a steady pop), mirrored onto the fur.</summary>
|
||||
public float XRayPulseStrength =>
|
||||
_materialInstance && _materialInstance.HasProperty(XRayPulseStrengthId)
|
||||
? _materialInstance.GetFloat(XRayPulseStrengthId) : 0f;
|
||||
|
||||
/// <summary>Authored X-ray pulse shape, mirrored onto the fur so its breathing matches the body.</summary>
|
||||
public float XRayPulseShape =>
|
||||
_materialInstance && _materialInstance.HasProperty(XRayPulseShapeId)
|
||||
? _materialInstance.GetFloat(XRayPulseShapeId) : 1f;
|
||||
|
||||
[Tooltip("Seconds for the see-through X-ray reveal glow to fade fully in or out when toggled (0 = instant snap).")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float xrayFadeDuration = 0.3f;
|
||||
@@ -431,7 +456,9 @@ public partial class MeshPainter : MonoBehaviour
|
||||
private static readonly int HalfLambertBrightnessId = Shader.PropertyToID("_HalfLambertBrightness");
|
||||
private static readonly int SpecularDampenId = Shader.PropertyToID("_SpecularDampen");
|
||||
private static readonly int XRayColorId = Shader.PropertyToID("_XRayColor");
|
||||
private static readonly int XRayPulseSpeedId = Shader.PropertyToID("_XRayPulseSpeed");
|
||||
private static readonly int XRayPulseStrengthId = Shader.PropertyToID("_XRayPulseStrength");
|
||||
private static readonly int XRayPulseShapeId = Shader.PropertyToID("_XRayPulseShape");
|
||||
private static readonly int BrushColorId = Shader.PropertyToID("_BrushColor");
|
||||
private static readonly int ColorWriteMaskId = Shader.PropertyToID("_ColorWriteMask");
|
||||
private static readonly int BrushCenterId = Shader.PropertyToID("_BrushCenter");
|
||||
|
||||
@@ -59,6 +59,28 @@ Shader "Custom/FurShell"
|
||||
_WindSpeed("Wind Speed", Range(0.0, 10.0)) = 2.0
|
||||
_WindFrequency("Wind Spatial Frequency", Range(0.0, 10.0)) = 1.0
|
||||
_WindDirection("Wind Direction (object XZ)", Vector) = (1, 0, 0, 0)
|
||||
|
||||
// Avatar world scale, pushed by FurShellRenderer (transform.lossyScale). Drives the curvature droop
|
||||
// so the curl stays proportional at any player size; 1 = authored size (see the vertex shader).
|
||||
[HideInInspector] _FurObjScale("Object Scale (driven by FurShellRenderer)", Float) = 1
|
||||
// How strongly the avatar scale drives the fur SIZE: 1 = fully proportional (fur shrinks/grows with the
|
||||
// avatar), 0 = constant world size (same absolute fur length at any avatar scale). Proportional fur
|
||||
// reads as too short on the small hider and too long on the big hunter, so this blends between them.
|
||||
[HideInInspector] _FurScaleInfluence("Scale Influence (driven by FurShellRenderer)", Range(0, 1)) = 0.5
|
||||
|
||||
[Header(XRay Reveal)]
|
||||
// See-through reveal glow mirrored from the sibling MeshPainter's body X-ray so the fur strands glow
|
||||
// WITH the reveal instead of hiding it under opaque fur. rgb = HDR glow colour, a = fade master.
|
||||
// Default (0,0,0,0) => additive term is zero, so the fur looks unchanged until a reveal fades in.
|
||||
[HDR] _XRayColor("X-Ray Overlay Color", Color) = (0, 0, 0, 0)
|
||||
// Breathing pulse kept identical to the body halves (PlayerCamo.hlsl / XRayOverlay) so they stay in phase.
|
||||
_XRayPulseSpeed("X-Ray Pulse Speed", Float) = 3
|
||||
_XRayPulseStrength("X-Ray Pulse Strength", Range(0, 1)) = 0.5
|
||||
_XRayPulseShape("X-Ray Pulse Shape", Range(1, 8)) = 3
|
||||
// The fur draws _ShellCount stacked TRANSPARENT shells; the additive glow below would composite once
|
||||
// PER overlapping layer and blow out (the body writes it once, opaque). This scales the per-shell glow
|
||||
// down so the stacked total reads at roughly the body's intensity. Tune to match the body glow.
|
||||
_XRayFurStrength("X-Ray Fur Strength (tames shell stacking)", Range(0, 1)) = 0.2
|
||||
}
|
||||
|
||||
SubShader
|
||||
@@ -127,6 +149,13 @@ Shader "Custom/FurShell"
|
||||
float _WindSpeed;
|
||||
float _WindFrequency;
|
||||
float4 _WindDirection;
|
||||
float _FurObjScale;
|
||||
float _FurScaleInfluence;
|
||||
half4 _XRayColor;
|
||||
half _XRayPulseSpeed;
|
||||
half _XRayPulseStrength;
|
||||
half _XRayPulseShape;
|
||||
half _XRayFurStrength;
|
||||
CBUFFER_END
|
||||
|
||||
TEXTURE2D(_BaseMap);
|
||||
@@ -173,13 +202,24 @@ Shader "Custom/FurShell"
|
||||
float count = max(_ShellCount, 1.0);
|
||||
float h = (count <= 1.0) ? 0.0 : (float)shellIndex / (count - 1.0);
|
||||
|
||||
// Effective fur-size scale. The instance matrix already multiplies the object-space fur
|
||||
// displacement by the avatar's world scale (_FurObjScale), so at full influence the fur is
|
||||
// fully proportional (small avatar => small fur, like the body). _FurScaleInfluence blends
|
||||
// that toward a CONSTANT world size (influence 0 => same absolute fur length at any avatar
|
||||
// size), because fully proportional fur reads as too short on the small hider and too long on
|
||||
// the big hunter. dispComp pre-divides the object-space offset so that after the matrix's
|
||||
// *_FurObjScale the NET world displacement equals effScale, not _FurObjScale.
|
||||
float objScale = max(_FurObjScale, 1e-4);
|
||||
float effScale = lerp(1.0, objScale, _FurScaleInfluence);
|
||||
float dispComp = effScale / objScale;
|
||||
|
||||
// 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));
|
||||
float3 posOS = input.positionOS.xyz + input.normalOS * (_ShellLength * (h + 0.02) * dispComp);
|
||||
|
||||
// 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,
|
||||
@@ -189,17 +229,20 @@ Shader "Custom/FurShell"
|
||||
float wlen = length(wdir);
|
||||
wdir = (wlen > 1e-4) ? (wdir / wlen) : float2(1.0, 0.0);
|
||||
float phase = _Time.y * _WindSpeed + (input.positionOS.x + input.positionOS.z) * _WindFrequency;
|
||||
posOS.xz += wdir * (_WindStrength * _ShellLength * h * h * sin(phase));
|
||||
posOS.xz += wdir * (_WindStrength * _ShellLength * h * h * sin(phase) * dispComp);
|
||||
|
||||
// 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;
|
||||
// Curl droop in world down. It uses the SAME effScale as the extrusion above (not the raw
|
||||
// _FurObjScale), so the curl keeps its SHAPE relative to the fur length at any avatar size and
|
||||
// at any influence setting - dialing the length toward constant carries the droop with it
|
||||
// instead of leaving it too strong on the shrunk hider / too weak on the grown hunter. Applied
|
||||
// after moving to world space, so the droop follows true world down regardless of player rotation.
|
||||
positionWS.y -= _Curvature * _ShellLength * h * h * effScale;
|
||||
|
||||
VertexNormalInputs vn = GetVertexNormalInputs(input.normalOS);
|
||||
|
||||
@@ -341,6 +384,18 @@ Shader "Custom/FurShell"
|
||||
half rim = pow(saturate(1.0h - saturate(dot(N, V))), _RimPower) * _RimStrength * h;
|
||||
color += rim * mainLight.color * albedo;
|
||||
|
||||
// X-Ray reveal glow on the VISIBLE fur, mirroring the body's forward half (PlayerCamo.hlsl)
|
||||
// EXACTLY so the strands glow IN PHASE with the body instead of hiding its reveal under
|
||||
// opaque fur. Additive HDR term; _XRayColor.a is the eased fade master (0 when off => fur
|
||||
// unchanged). FurShellRenderer pushes the same colour/pulse/flash state the sibling
|
||||
// MeshPainter drives on the body, so on/occluded stays consistent between fur and body.
|
||||
half xrayCurve = pow(0.5h - 0.5h * cos(_Time.y * _XRayPulseSpeed), _XRayPulseShape);
|
||||
half xrayPulse = 1.0h - _XRayPulseStrength * (1.0h - xrayCurve);
|
||||
// _XRayFurStrength tames the per-shell accumulation: the body adds this term once (opaque),
|
||||
// but the fur stacks _ShellCount transparent shells that each composite it, so the raw term
|
||||
// over-brightens. Scaling it per shell brings the stacked total back near the body's glow.
|
||||
color += _XRayColor.rgb * (_XRayColor.a * xrayPulse * _XRayFurStrength);
|
||||
|
||||
return half4(color, alpha);
|
||||
}
|
||||
ENDHLSL
|
||||
|
||||
Reference in new issue
Block a user