#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); }