/**
* Сборщик пост-прохода (движок жанронезависим): из включённых эффектов
* собирается фрагментный шейдер + стартовые юниформы для
* VoxelRenderer.setPostPass. Эффекты детерминированы: «зерно» — статичный
* хэш по координатам пикселя (без времени), анимируется только дисторсия
* через uTime — при замороженном uTime кадр-равенства гейтов не падают.
*/
/** Включённые эффекты (поле !== undefined — эффект в шейдере) и их стартовые силы. */
export interface PostOptions {
/** Виньетка: затемнение к углам (dot(d,d)·2; 0.3–0.5 — заметная). */
vignette?: number;
/** Зерно: ± величина к цвету (0.02–0.05 — едва заметное). */
grain?: number;
/** Дисторсия «жары»: горизонтальное дрожание к низу кадра (амплитуда). */
wobble?: number;
}
/** Готовый проход: шейдер + юниформы (uTime игра тикает сама). */
export interface PostPassSpec {
fragment: string;
uniforms: Record<string, { value: number }>;
}
/** Шапка шейдера: uv с квада, сэмпл сцены, время (дисторсия). */
const HEAD = `
varying vec2 vUv;
uniform sampler2D tDiffuse;
uniform float uTime;
void main() {
vec2 uv = vUv;
`;
/** Дисторсия: сдвиг uv ДО сэмпла, сильнее к низу кадра (земля/машина). */
const WOBBLE = `
uv.x += sin(uv.y * 36.0 - uTime * 2.4) * uWobble * (1.2 - uv.y);`;
/** Сэмпл и выход — общий хвост. */
const OUT = `
vec3 c = texture2D(tDiffuse, uv).rgb;
`;
const VIGNETTE = `
vec2 d = vUv - 0.5;
c *= 1.0 - uVignette * dot(d, d) * 2.0;`;
const GRAIN = `
float n = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;
c += n * uGrain;`;
const END = `
gl_FragColor = vec4(c, 1.0);
}`;
/** Собрать пост-проход: шейдер из включённых эффектов + стартовые юниформы. */
export function postPass(o: PostOptions = {}): PostPassSpec {
const decls: string[] = [];
const body: string[] = [];
const uniforms: PostPassSpec['uniforms'] = {};
if (o.wobble !== undefined) {
decls.push('uniform float uWobble;');
body.push(WOBBLE);
uniforms.uWobble = { value: o.wobble };
}
body.push(OUT);
if (o.vignette !== undefined) {
decls.push('uniform float uVignette;');
body.push(VIGNETTE);
uniforms.uVignette = { value: o.vignette };
}
if (o.grain !== undefined) {
decls.push('uniform float uGrain;');
body.push(GRAIN);
uniforms.uGrain = { value: o.grain };
}
body.push(END);
return { fragment: decls.join('\n') + '\n' + HEAD + body.join(''), uniforms };
}