Newer
Older
rpg / v2 / packages / engine / src / render / postShader.ts
/**
 * Сборщик пост-прохода (движок жанронезависим): из включённых эффектов
 * собирается фрагментный шейдер + стартовые юниформы для
 * 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);`;
/** Сэмпл и выход — общий хвост. Сцена рендерится в RT без выходной
 * конверсии (linear), канвас ждёт sRGB: конверсия делается СРАЗУ после
 * сэмпла — эффекты (виньетка/зерно) работают на отображаемом кадре, иначе
 * виньетка в linear выглядит вдвое слабее задуманного. three кладёт
 * linearToOutputTexel в преамбулу фрагментного шейдера любого материала,
 * включая ShaderMaterial. Без конверсии кадр темнеет ~втрое. */
const OUT = `
    vec3 c = linearToOutputTexel(vec4(texture2D(tDiffuse, uv).rgb, 1.0)).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 };
}