Files
Vibe-HD-2D/assets/shaders/terrain.frag
Gabrieal Jimmy 2c99305fb0 init
2026-10-02 12:42:00 -05:00

97 lines
3.9 KiB
GLSL

#version 450 core
#include "common.glsl"
#include "pixelart.glsl"
in VS {
vec3 worldPos;
vec3 normal;
vec2 uv;
flat float layer;
float ao;
flat float kind;
} v;
layout(binding = 0) uniform sampler2DArray uTerrainTex;
layout(binding = 1) uniform usampler2D uMatMap;
uniform ivec2 uMapSize;
uniform ivec4 uMatTop[8]; // topFirst, topCount, accentFirst, accentCount
uniform float uMatAccent[8]; // accent chance
layout(location = 0) out vec4 fragColor;
// Picks the material of the pixel-art texel at `wp`. Tile borders are jittered
// with low-frequency noise plus chunky per-4px offsets so transitions look
// hand-drawn instead of grid aligned.
float topLayer(vec3 wp) {
float texels = float(textureSize(uTerrainTex, 0).x);
vec2 sp = (floor(wp.xz * texels) + 0.5) / texels;
vec2 j = vec2(vnoise(sp * 1.7), vnoise(sp * 1.7 + vec2(31.7, 11.3))) - 0.5;
vec2 chunk = floor(sp * texels / 4.0);
j += (vec2(hash12(chunk), hash12(chunk + 7.7)) - 0.5) * 0.3;
ivec2 cell = clamp(ivec2(floor(sp + j * 0.55)), ivec2(0), uMapSize - 1);
uint m = min(texelFetch(uMatMap, cell, 0).r, 7u);
ivec4 mt = uMatTop[m];
vec2 c = vec2(cell);
int layer = mt.x + int(hash12(c + 0.37) * float(mt.y)) % max(mt.y, 1);
if (mt.w > 0 && hash12(c * 1.37 + 5.1) < uMatAccent[m])
layer = mt.z + int(hash12(c + 9.3) * float(mt.w)) % mt.w;
return float(layer);
}
void main() {
vec3 wp = v.worldPos;
vec3 n = normalize(v.normal);
vec4 albedo;
if (v.kind < 0.5) {
albedo = sharpSampleArray(uTerrainTex, wp.xz, topLayer(wp));
} else {
albedo = sharpSampleArray(uTerrainTex, v.uv, v.layer);
if (v.kind > 1.5) {
// Ragged overhang strip (e.g. grass hanging over a cliff edge).
float texels = float(textureSize(uTerrainTex, 0).x);
float px = floor(v.uv.x * texels);
float lip = 2.0 + floor(hash12(vec2(px, v.layer)) * 2.5) + floor(vnoise(vec2(px * 0.3, 3.0)) * 3.0);
float row = v.uv.y * texels;
if (row < lip) {
albedo = sharpSampleArray(uTerrainTex, vec2(v.uv.x, wp.y), v.kind - 2.0);
albedo.rgb *= mix(1.0, 0.7, row / lip);
} else if (row < lip + 1.0) {
albedo.rgb *= 0.55; // dark contact line under the lip
}
}
}
vec3 L = uSunDir.xyz;
float ndl = max(dot(n, L), 0.0);
float vis = sunVisibility(wp + n * 0.03);
vec3 sun = uSunColor.rgb * uSunDir.w * ndl * vis;
// Camera-side fill keeps cliff faces readable when the sun is behind them.
vec3 toCam = cross(uBillboardRight.xyz, vec3(0.0, 1.0, 0.0));
vec3 amb = (hemiAmbient(n) + uSkyHorizon.rgb * uSkyHorizon.w * max(dot(n, toCam), 0.0)) * v.ao;
float gao = groundAO(wp, v.kind < 0.5 ? 0.0 : 0.6);
vec3 sheen = vec3(0.0);
vec3 base = albedo.rgb;
if (uWeather.y > 0.0 && wp.y > uWaterParams.x)
base = wetSurface(base, n, normalize(uCamPos.xyz - wp), vis, sheen);
// Contact AO dims ambient fully and direct light partially (reads as a soft contact shadow).
vec3 col = base * (sun * mix(1.0, gao, 0.7) + amb * gao + pointLights(wp, n)) + sheen * gao;
// Caustics on underwater surfaces, projected along the sun direction.
if (uWaterParams.z > 0.5 && wp.y < uWaterParams.x) {
float depth = uWaterParams.x - wp.y;
vec2 cp = wp.xz + L.xz / max(L.y, 0.2) * depth;
float c = caustics(cp * 0.33, uTime * 0.55);
float vSurf = sunVisibility(wp + L * (depth / max(L.y, 0.2)));
col += albedo.rgb * uSunColor.rgb * uSunDir.w * c * uWaterParams.y * vSurf *
exp(-depth * 0.7) * max(n.y, 0.35);
}
// Soft contact shadow under the player sprite.
if (uBlob.w > 0.0) {
float d = length(wp.xz - uBlob.xz) / uBlob.w;
float onFloor = 1.0 - smoothstep(0.2, 0.6, abs(wp.y - uBlob.y));
col *= 1.0 - 0.5 * (1.0 - smoothstep(0.3, 1.0, d)) * onFloor;
}
fragColor = vec4(col, 1.0);
}