Skip to content

Instantly share code, notes, and snippets.

@nenadvulic
Created December 21, 2025 18:56
Show Gist options
  • Select an option

  • Save nenadvulic/848d8d12335783ff70484e7a3572c8c7 to your computer and use it in GitHub Desktop.

Select an option

Save nenadvulic/848d8d12335783ff70484e7a3572c8c7 to your computer and use it in GitHub Desktop.
under water shader
#include <metal_stdlib>
using namespace metal;
struct VertexOut {
float4 position [[position]];
float2 uv;
};
struct UnderwaterUniforms {
float2 viewportSize;
float time;
float strength;
float caustics;
float absorption;
float turbidity;
float waterLevel;
float edgeFeather;
float edgeWaves;
float2 _pad;
};
vertex VertexOut fullscreenVertex(
const device float* vtx [[buffer(0)]],
uint vid [[vertex_id]]
) {
VertexOut out;
uint i = vid * 4;
out.position = float4(vtx[i + 0], vtx[i + 1], 0.0, 1.0);
out.uv = float2(vtx[i + 2], vtx[i + 3]);
return out;
}
inline float hash21(float2 p) {
p = fract(p * float2(123.34, 345.45));
p += dot(p, p + 34.345);
return fract(p.x * p.y);
}
inline float noise(float2 p) {
float2 i = floor(p);
float2 f = fract(p);
float a = hash21(i);
float b = hash21(i + float2(1, 0));
float c = hash21(i + float2(0, 1));
float d = hash21(i + float2(1, 1));
float2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
inline float fbm(float2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 4; i++) {
v += a * noise(p);
p *= 2.02;
a *= 0.5;
}
return v;
}
inline float causticPattern(float2 uv, float t) {
float2 p = uv * 6.0;
float n1 = fbm(p + float2(t * 0.20, t * 0.13));
float n2 = fbm(p * 1.4 - float2(t * 0.15, t * 0.22));
return pow(saturate(1.0 - abs(n1 - n2) * 2.2), 3.0);
}
inline float3 underwaterShade(float2 uv, float2 px, float t,
texture2d<float> tex, sampler samp,
constant UnderwaterUniforms& u) {
float2 p = uv * 2.0;
float w1 = sin((p.x * 8.0 + t * 1.2) + sin(p.y * 3.0 + t * 0.7));
float w2 = sin((p.y * 10.0 + t * 1.0) + sin(p.x * 4.0 - t * 0.6));
float w3 = fbm(p * 3.0 + float2(t * 0.25, -t * 0.18)) * 2.0 - 1.0;
float2 refr = float2(w1 + w3, w2 - w3);
float dist = (0.8 + 2.6 * saturate(u.strength)) * 6.0;
float2 uv2 = uv + refr * px * dist;
float3 rgb = tex.sample(samp, uv2).rgb;
float depth = saturate((uv.y - u.waterLevel) / max(1e-4, (1.0 - u.waterLevel)));
float absorb = saturate(u.absorption) * (0.25 + 0.75 * depth);
float3 waterTint = float3(0.07, 0.35, 0.45);
rgb = mix(rgb, rgb * (1.0 - 0.35 * absorb) + waterTint * (0.55 * absorb), absorb);
float c = causticPattern(uv + refr * 0.05, t);
float ca = saturate(u.caustics) * (0.30 + 0.50 * (1.0 - depth));
rgb += c * ca;
float haze = saturate(u.turbidity) * (0.12 + 0.55 * depth);
float fogN = fbm(uv * 5.5 + float2(t * 0.03, -t * 0.02));
float3 hazeCol = waterTint + fogN * 0.06;
rgb = mix(rgb, hazeCol, haze);
float sp = step(0.987, hash21(floor(uv * 130.0 + t * 8.0)));
rgb += sp * 0.05 * haze;
return saturate(rgb);
}
fragment float4 underwaterSplitFragment(
VertexOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]],
constant UnderwaterUniforms& u [[buffer(0)]]
) {
float2 uv = in.uv;
float4 base = tex.sample(samp, uv);
float2 px = 1.0 / max(u.viewportSize, float2(1.0));
float waveAmp = (0.004 + 0.012 * u.edgeWaves);
float wave =
sin(uv.x * 12.0 + u.time * 1.2) * 0.5 +
sin(uv.x * 24.0 - u.time * 0.9) * 0.35 +
(fbm(float2(uv.x * 6.0, u.time * 0.3)) - 0.5) * 0.6;
float waterline = u.waterLevel + wave * waveAmp;
float feather = max(u.edgeFeather, 0.0);
float m = smoothstep(waterline - feather, waterline + feather, uv.y);
float3 uw = underwaterShade(uv, px, u.time, tex, samp, u);
float3 outRGB = mix(base.rgb, uw, m);
float rim = 1.0 - smoothstep(0.0, feather * 2.5 + 1e-4, fabs(uv.y - waterline));
outRGB += rim * 0.05 * u.edgeWaves;
return float4(saturate(outRGB), base.a);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment