Skip to content

Commit 50c01b6

Browse files
committed
initial version of fur shaders
1 parent da55a7a commit 50c01b6

13 files changed

Lines changed: 2331 additions & 0 deletions

example/lib/shader_cards.dart

Lines changed: 276 additions & 0 deletions
Large diffs are not rendered by default.

example/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,5 @@ flutter:
5050
- packages/material_palette/shaders/radial_pixel_dissolve.frag
5151
- packages/material_palette/shaders/tappable_pixel_dissolve.frag
5252
- packages/material_palette/shaders/tappable_slurp.frag
53+
- packages/material_palette/shaders/fur_planar.frag
54+
- packages/material_palette/shaders/fur_planar_mask.frag

fur_planar.frag

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
#include <flutter/runtime_effect.glsl>
2+
3+
precision highp float;
4+
5+
uniform vec2 uSize;
6+
uniform float time;
7+
uniform vec3 uBgColor; // sRGB space input
8+
9+
// Plane shape uniforms
10+
uniform float uPlaneOffset;
11+
uniform float uFurThickness;
12+
13+
// Fur pattern uniforms
14+
uniform float uFurNoiseStrength;
15+
uniform float uFurNoiseScale; // Controls hair fineness (higher = thinner hairs)
16+
uniform float uFurWaveAmplitude;
17+
uniform float uFurWaveFreqX;
18+
uniform float uFurWaveFreqY;
19+
uniform float uFurAnimationSpeed;
20+
21+
// Key light uniforms
22+
uniform vec3 uKeyLightDir;
23+
uniform vec3 uKeyLightColor;
24+
uniform float uKeyLightIntensity;
25+
26+
// Fill light uniforms
27+
uniform vec3 uFillLightDir;
28+
uniform vec3 uFillLightColor;
29+
uniform float uFillLightIntensity;
30+
31+
// Rim/back light uniforms
32+
uniform vec3 uRimLightDir;
33+
uniform vec3 uRimLightColor;
34+
uniform float uRimLightIntensity;
35+
36+
// Fur color uniform
37+
uniform vec3 uFurColor;
38+
39+
// Gradient epsilon uniform
40+
uniform float uGradientEps;
41+
42+
// Wavelet parameter uniforms
43+
uniform float uWaveletSpeed;
44+
uniform float uWaveletFreq;
45+
uniform float uWaveletAmplitude;
46+
uniform float uWaveletDecay;
47+
uniform float uWaveletWidth;
48+
49+
// Click/wavelet uniforms
50+
uniform float uClickCount;
51+
uniform vec2 uClickPos0;
52+
uniform vec2 uClickPos1;
53+
uniform vec2 uClickPos2;
54+
uniform vec2 uClickPos3;
55+
uniform vec2 uClickPos4;
56+
uniform float uClickTime0;
57+
uniform float uClickTime1;
58+
uniform float uClickTime2;
59+
uniform float uClickTime3;
60+
uniform float uClickTime4;
61+
62+
out vec4 fragColor;
63+
64+
#define V vec3
65+
66+
// ============ sRGB CONVERSION ============
67+
68+
// sRGB to linear (for lighting calculations)
69+
V srgbToLinear(V srgb) {
70+
return pow(srgb, V(2.2));
71+
}
72+
73+
// Linear to sRGB (for output)
74+
V linearToSrgb(V linear) {
75+
return pow(max(linear, V(0.0)), V(1.0 / 2.2));
76+
}
77+
78+
// ============ CONFIGURATION ============
79+
80+
// Lighting - fixed parameters
81+
const float LIGHT_INITIAL = 5.0;
82+
const float LIGHT_ABSORPTION = 3.0;
83+
const float ALPHA_MULTIPLIER = 2.0;
84+
85+
// Front light: from camera position
86+
const float FRONT_INTENSITY = 0.35;
87+
const V FRONT_COLOR = V(1.0, 0.98, 0.95);
88+
89+
// Ambient (relative to background color)
90+
const float AMBIENT_STRENGTH = 0.6;
91+
92+
// Raymarching
93+
const float RAY_STEP = 0.025;
94+
const int RAY_STEPS = 64;
95+
const float CAMERA_DISTANCE = 3.0;
96+
97+
// ============ PROCEDURAL NOISE ============
98+
99+
float hash21(vec2 p) {
100+
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
101+
p3 += dot(p3, p3.yzx + 33.33);
102+
return fract((p3.x + p3.y) * p3.z);
103+
}
104+
105+
float proceduralNoise(vec2 p) {
106+
vec2 i = floor(p);
107+
vec2 f = fract(p);
108+
vec2 u = f * f * (3.0 - 2.0 * f);
109+
110+
float a = hash21(i);
111+
float b = hash21(i + vec2(1.0, 0.0));
112+
float c = hash21(i + vec2(0.0, 1.0));
113+
float d = hash21(i + vec2(1.0, 1.0));
114+
115+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
116+
}
117+
118+
// ============ SHADER CODE ============
119+
120+
// Signed distance function for the plane half-space.
121+
// The solid region is z > planeZ (behind the plane).
122+
// Fur grows toward the camera (in -z direction) from the plane surface.
123+
// Returns: positive in front of plane (toward camera), negative behind.
124+
float sdPlane(float pz) {
125+
return -uPlaneOffset - pz;
126+
}
127+
128+
// Calculate wavelet displacement from a single click (3D version)
129+
float wavelet(V pos, vec2 clickPos, float clickTime) {
130+
if (clickTime <= 0.0) return 0.0;
131+
float decay = exp(-clickTime * uWaveletDecay);
132+
if (decay < 0.001) return 0.0;
133+
134+
vec2 dxy = pos.xy - clickPos;
135+
float dist = sqrt(dxy.x * dxy.x + dxy.y * dxy.y + pos.z * pos.z);
136+
float waveRadius = clickTime * uWaveletSpeed;
137+
138+
float ringDist = abs(dist - waveRadius);
139+
if (ringDist > uWaveletWidth) return 0.0;
140+
float ring = 1.0 - ringDist / uWaveletWidth;
141+
142+
float wave = sin(dist * uWaveletFreq - waveRadius * uWaveletFreq);
143+
144+
return wave * ring * decay * uWaveletAmplitude;
145+
}
146+
147+
// Calculate total wavelet displacement from all clicks
148+
float totalWavelet(V pos) {
149+
int clicks = int(uClickCount);
150+
if (clicks == 0) return 0.0;
151+
152+
float total = wavelet(pos, uClickPos0, uClickTime0);
153+
if (clicks == 1) return total;
154+
total += wavelet(pos, uClickPos1, uClickTime1);
155+
if (clicks == 2) return total;
156+
total += wavelet(pos, uClickPos2, uClickTime2);
157+
if (clicks == 3) return total;
158+
total += wavelet(pos, uClickPos3, uClickTime3);
159+
if (clicks == 4) return total;
160+
total += wavelet(pos, uClickPos4, uClickTime4);
161+
162+
return total;
163+
}
164+
165+
// Base shape + noise (for gradient - gives fur strand detail)
166+
float fForGradient(V p) {
167+
float baseShape = uFurThickness - sdPlane(p.z);
168+
float furNoise = uFurNoiseStrength * proceduralNoise(p.xy * uFurNoiseScale);
169+
float furWave = uFurWaveAmplitude * sin(
170+
sin(uFurWaveFreqX * p.x) +
171+
uFurWaveFreqY * p.y +
172+
uFurAnimationSpeed * time
173+
);
174+
return baseShape - furNoise - furWave;
175+
}
176+
177+
// Full density function for the furry plane
178+
float f(V p, float waveletDisp) {
179+
return max(0.0, fForGradient(p) + waveletDisp);
180+
}
181+
182+
// Gradient using forward differences (3 samples instead of 6)
183+
V fastGradient(V p) {
184+
float center = fForGradient(p);
185+
return V(
186+
center - fForGradient(p + V(uGradientEps, 0.0, 0.0)),
187+
center - fForGradient(p + V(0.0, uGradientEps, 0.0)),
188+
center - fForGradient(p + V(0.0, 0.0, uGradientEps))
189+
);
190+
}
191+
192+
// Compute wavelet gradient contribution (for visible ripple effect)
193+
V waveletGradient(V p) {
194+
float center = totalWavelet(p);
195+
return V(
196+
center - totalWavelet(p + V(uGradientEps, 0.0, 0.0)),
197+
center - totalWavelet(p + V(0.0, uGradientEps, 0.0)),
198+
center - totalWavelet(p + V(0.0, 0.0, uGradientEps))
199+
);
200+
}
201+
202+
void main() {
203+
vec2 fragCoord = FlutterFragCoord().xy;
204+
205+
// Convert background color from sRGB to linear for lighting calculations
206+
V bgLinear = srgbToLinear(uBgColor);
207+
208+
float minDim = min(uSize.x, uSize.y);
209+
float invMinDim = 1.0 / minDim;
210+
211+
// Normalize light directions (hoisted outside loop)
212+
V keyDir = normalize(uKeyLightDir);
213+
V fillDir = normalize(uFillLightDir);
214+
V rimDir = normalize(uRimLightDir);
215+
216+
// Pre-compute rim color blend with background
217+
V rimColorLinear = mix(uRimLightColor, bgLinear, 0.5);
218+
219+
// Pre-compute ambient
220+
V ambient = bgLinear * AMBIENT_STRENGTH;
221+
222+
// Ray setup
223+
vec2 uv = (fragCoord - 0.5 * uSize) * invMinDim;
224+
V rayDir = V(uv, 1.0);
225+
V rayPos = rayDir * 2.3 - V(0.0, 0.0, CAMERA_DISTANCE);
226+
V rayStep = rayDir * RAY_STEP;
227+
228+
// Light accumulator (for self-shadowing within fur)
229+
V lightAccum = V(LIGHT_INITIAL);
230+
231+
// Volumetric compositing
232+
V accumulatedColor = V(0.0);
233+
float transmittance = 1.0;
234+
235+
// Raymarching loop
236+
for (int i = 0; i < RAY_STEPS; i++) {
237+
// March forward
238+
rayPos += rayStep;
239+
240+
// Calculate wavelet once per step, reuse for density
241+
float waveletDisp = totalWavelet(rayPos);
242+
float density = f(rayPos, waveletDisp);
243+
244+
// Attenuate light through fur (for self-shadowing)
245+
lightAccum *= uFurColor - density / LIGHT_ABSORPTION;
246+
247+
// Compute gradient: base shape + wavelet contribution for visible ripples
248+
V gradient = fastGradient(rayPos);
249+
250+
// Only compute wavelet gradient when there are active clicks
251+
if (uClickCount > 0.0) {
252+
gradient += waveletGradient(rayPos);
253+
}
254+
255+
// Surface brightness from gradient magnitude
256+
float gradMag = length(gradient);
257+
V normal = normalize(gradient + 0.001);
258+
259+
// Cache normalized ray position and view direction
260+
V rayPosNorm = normalize(rayPos);
261+
V viewDir = -rayPosNorm;
262+
263+
// Four-point lighting calculation
264+
// Key light (with shadow occlusion)
265+
float keyDiffuse = max(0.0, dot(normal, keyDir));
266+
float keyShadow = smoothstep(-0.3, 0.5, dot(rayPosNorm, keyDir));
267+
V keyContrib = uKeyLightColor * (uKeyLightIntensity * keyDiffuse * keyShadow);
268+
269+
// Front light (from camera position)
270+
float NdotV = max(0.0, dot(normal, viewDir));
271+
V frontContrib = FRONT_COLOR * (FRONT_INTENSITY * NdotV);
272+
273+
// Fill light (softer, less shadow)
274+
float fillDiffuse = max(0.0, dot(normal, fillDir));
275+
float fillShadow = 0.5 + 0.5 * smoothstep(-0.5, 0.3, dot(rayPosNorm, fillDir));
276+
V fillContrib = uFillLightColor * (uFillLightIntensity * fillDiffuse * fillShadow);
277+
278+
// Back/rim light
279+
float oneMinusNdotV = 1.0 - NdotV;
280+
float rim = oneMinusNdotV * oneMinusNdotV;
281+
float backDiffuse = max(0.0, dot(normal, rimDir));
282+
float rimShadow = smoothstep(-0.3, 0.5, dot(rayPosNorm, rimDir));
283+
V backContrib = rimColorLinear * (uRimLightIntensity * rim * (0.5 + 0.5 * backDiffuse) * rimShadow);
284+
285+
// Combine all lights with ambient
286+
V totalLight = ambient + keyContrib + frontContrib + fillContrib + backContrib;
287+
288+
// Opacity for this sample
289+
float densityOpacity = density * ALPHA_MULTIPLIER * 0.5;
290+
float strandOpacity = gradMag * ALPHA_MULTIPLIER * 0.15 * step(0.001, density);
291+
float sampleOpacity = clamp(max(densityOpacity, strandOpacity), 0.0, 1.0);
292+
293+
// Color contribution from this sample
294+
V sampleColor = lightAccum * totalLight;
295+
296+
// Front-to-back compositing
297+
accumulatedColor += transmittance * sampleOpacity * sampleColor;
298+
299+
// Reduce transmittance
300+
transmittance *= (1.0 - sampleOpacity);
301+
302+
// Early termination when nearly opaque
303+
if (transmittance < 0.01) break;
304+
}
305+
306+
// Tone map the accumulated fur color
307+
V furToneMapped = accumulatedColor / (1.0 + accumulatedColor);
308+
309+
// Composite with background
310+
V finalLinear = furToneMapped + bgLinear * transmittance;
311+
312+
// Convert from linear to sRGB for output
313+
fragColor = vec4(linearToSrgb(finalLinear), 1.0);
314+
}

0 commit comments

Comments
 (0)