subsurface scattering foliage with trampling and wind
set the TramplePos global to your player position
subsurface based on: https://godotshaders.com/shader/simple-foliage-subsurface-scattering-shader/
quick fix 2/262/2026:
changed height mask to uv space because i forgot im using world vertex coords and changed opacity texture to filter_nearest instead of filter_nearest_mipmap
Shader code
shader_type spatial;
render_mode cull_disabled, world_vertex_coords;
global uniform vec3 TramplePos;
uniform float TrampleFalloff = 2.0;
uniform float TrampleRadius = 1.0;
uniform float TrampleStrength = 1.5;
uniform sampler2D Tex : source_color, filter_nearest;
uniform sampler2D NormalTex : hint_normal, filter_nearest;
uniform sampler2D OpacityTex: filter_nearest;
uniform sampler2D ThicknessTex;
uniform bool UseThicknessMap = false;
uniform float Sigma : hint_range(0.,1.,.01) = .5;
uniform float Power : hint_range(0.,100.,.25) = 5.;
uniform float Scale : hint_range(0.,50.,.1) = 3.3;
uniform bool UseOpacity = true;
uniform float AlphaCutoff : hint_range(0.,1.,.01) = .1;
uniform float AttenuationInfluence : hint_range(0.,1.,.01) = 1.;
uniform float AlbedoInfluence : hint_range(0.,1.,.01) = .7;
uniform float LightColorInfluence : hint_range(0.,1.,.01) = .3;
uniform bool DoVertexSway = true;
uniform float VertexSwayIntensity : hint_range(0.0, 10.0, 0.01) = .3;
uniform float GustScale = 0.05; // size of gust waves
uniform float GustSpeed = 0.8; // how fast gust travels
uniform float GustStrength = 1.5; // intensity multiplier
uniform float GustFrequency = 0.5; // how often gust pulses
uniform vec2 WindDirection = vec2(1.0, 0.3); // world wind direction
varying vec4 VtxColor;
varying vec3 NormalWs;
varying mat3 Tbn;
float Hash(vec2 P)
{
P = fract(P * vec2(123.34, 345.45));
P += dot(P, P + 34.345);
return fract(P.x * P.y);
}
float Noise(vec2 P)
{
vec2 I = floor(P);
vec2 F = fract(P);
float A = Hash(I);
float B = Hash(I + vec2(1.0, 0.0));
float C = Hash(I + vec2(0.0, 1.0));
float D = Hash(I + vec2(1.0, 1.0));
vec2 U = F * F * (3.0 - 2.0 * F);
return mix(A, B, U.x) +
(C - A) * U.y * (1.0 - U.x) +
(D - B) * U.x * U.y;
}
float GetSss(vec3 N, vec3 V, vec3 L, float S)
{
L = normalize(L);
float Ksss = pow(clamp(dot((-L + N * S), V), 0., 1.), Power);
return Ksss;
}
void vertex()
{
VtxColor = COLOR;
Tbn = mat3(TANGENT, BINORMAL, NORMAL);
if (DoVertexSway)
{
float HeightMask = pow(clamp( 1.0 - UV.y, 0.0, 1.0),1.5);
vec2 Dir = normalize(WindDirection);
vec2 NoiseUV = VERTEX.xz * GustScale +
Dir * TIME * GustSpeed;
float GustNoise =
Noise(NoiseUV) * 0.6 +
Noise(NoiseUV * 2.0) * 0.3 +
Noise(NoiseUV * 4.0) * 0.1;
GustNoise = GustNoise * 2.0 - 1.0;
float Micro = sin(TIME * 2.0 + VERTEX.x * 0.3) * 0.2;
vec2 WindForce = Dir * GustNoise * GustStrength +
vec2(Micro, Micro * 0.5);
VERTEX.xz += WindForce * HeightMask * VertexSwayIntensity;
float Dist = distance(VERTEX, TramplePos);
if (Dist < TrampleRadius)
{
float Influence = 1.0 - (Dist / TrampleRadius);
Influence = pow(Influence, TrampleFalloff);
Influence *= HeightMask;
vec3 Dir = normalize(VERTEX - TramplePos);
Dir.y = 0.0;
Dir = normalize(Dir);
float DownAmount = Influence * TrampleStrength;
float SideAmount = Influence * TrampleStrength * 0.5;
VERTEX.y -= DownAmount;
VERTEX += Dir * SideAmount;
}
}
}
void fragment()
{
vec4 C = texture(Tex, UV);
float O = (UseOpacity) ? texture(OpacityTex, UV).r : C.a;
if (O < AlphaCutoff)
discard;
ALBEDO = C.rgb;
vec3 N = (1. - texture(NormalTex, UV).rgb) * 2. - 1.;
N = normalize(Tbn * N);
NormalWs = N;
}
void light()
{
vec3 N2 = normalize(vec3(0, 1, 0) + .5 * NormalWs);
float Kd = max(dot(normalize(LIGHT), N2), 0.0);
float S = (UseThicknessMap) ? 1. - texture(ThicknessTex, UV).r : Sigma;
float Ksss = GetSss(NormalWs, VIEW, LIGHT, S);
vec3 SssColor = AlbedoInfluence * ALBEDO +
LightColorInfluence * LIGHT_COLOR;
DIFFUSE_LIGHT += SssColor *
(max(mix(1., ATTENUATION, AttenuationInfluence), 0.001) *
(.3 + max(Kd * Scale, Ksss * Scale)));
}
