97 lines
3.9 KiB
GLSL
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);
|
|
}
|