Files
netrunner-landing/matrix-engine/shaders.js
T
2026-07-07 18:49:09 +07:00

711 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Файл содержит все исходные коды шейдеров для проекта Matrix.
* Разделение помогает фокусироваться на графике отдельно от логики.
*/
/**
* Вершинный шейдер капель матричного дождя. Рисует каждую каплю как
* инстансированный quad (2 треугольника, позиция берётся из a_position —
* см. STRIDE=5 в lib.rs и раскладку атрибутов в initWebGL()). Неочевидное:
* - UV-координаты в атлас (v_uv) считаются прямо здесь из a_charIdx и
* размера шрифта, а не передаются готовыми — атлас "нарезается" по
* формуле char_idx * fontSize по X, конкретная строка добавляется уже во
* фрагментном шейдере через a_atlasRow.
* - Лёгкое "притяжение" символов к курсору (pushOffset/hoverScale): каждый
* инстанс сам проверяет расстояние до u_mouse в clip-пространстве с учётом
* аспекта экрана и слегка сдвигается/увеличивается — это чисто визуальный
* эффект на уровне вершин, независимый от физики капель в WASM.
*/
export const vsMatrix = `#version 300 es
precision mediump float;
in vec2 a_quad;
in vec2 a_position;
in float a_charIdx;
in float a_atlasRow;
in float a_scale;
uniform vec2 u_resolution;
uniform float u_fontSize;
uniform vec2 u_mouse;
out vec2 v_uv;
out float v_atlasRow;
out float v_dist;
void main() {
float size = u_fontSize * a_scale;
vec2 res = max(u_resolution, vec2(1.0));
vec2 centerPos = a_position + vec2(size * 0.5);
vec2 zeroToOneCenter = centerPos / res;
vec2 aspect = vec2(res.x / res.y, 1.0);
vec2 diff = zeroToOneCenter * aspect - u_mouse * aspect;
float dist = length(diff);
v_dist = dist;
vec2 pushOffset = vec2(0.0);
float hoverScale = 1.0;
if (u_mouse.x > 0.001 && u_mouse.y > 0.001 && dist < 0.15 && dist > 0.0001) {
float force = 1.0 - smoothstep(0.0, 0.15, dist);
vec2 dir = diff / dist;
pushOffset = dir * force * 40.0;
hoverScale = 1.0 + force * 0.8;
}
vec2 finalPos = a_position + pushOffset + a_quad * (size * hoverScale);
vec2 zeroToOne = finalPos / res;
vec2 zeroToTwo = zeroToOne * 2.0;
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
v_uv = vec2(a_charIdx * u_fontSize + a_quad.x * u_fontSize, a_quad.y * u_fontSize);
v_atlasRow = a_atlasRow;
}`.trim();
/**
* Вершинный шейдер полноэкранного прохода пост-процессинга. Рисует один
* quad на весь clip space (-1..1) без всякой трансформации — вся работа
* пост-процесса происходит во фрагментном шейдере (postProcessSource),
* этот шейдер лишь превращает clip-координаты в UV (0..1) для выборки из
* промежуточной текстуры postTexture.
*/
export const vsOverlay = `#version 300 es
precision mediump float;
in vec2 a_position;
out vec2 v_uv;
void main() {
v_uv = a_position * 0.5 + 0.5;
gl_Position = vec4(a_position, 0.0, 1.0);
}`.trim();
/**
* Вершинный шейдер глитч-блоков (VHS-подобные цветные прямоугольники,
* см. GlitchBlock в lib.rs, буфер stride=5: x, y, w, h, color_type).
* Неочевидное: newSize = originalSize * 3.0 — геометрия рисуется втрое
* крупнее размеров w/h, посчитанных в Rust, а сдвиг offset центрирует
* увеличенный quad вокруг исходной позиции. Так мелкие "осколки" из lib.rs
* визуально превращаются в заметные, хорошо читаемые полосы-глитчи без
* необходимости считать увеличенный размер уже на стороне симуляции.
*/
export const vsGlitch = `#version 300 es
in vec2 a_quad;
in vec4 a_glitchData;
in float a_colorType;
uniform vec2 u_resolution;
out float v_colorType;
void main() {
vec2 originalSize = vec2(a_glitchData.z, a_glitchData.w);
vec2 newSize = originalSize * 3.0;
vec2 offset = (newSize - originalSize) * 0.5;
vec2 pixelPos = vec2(a_glitchData.x, a_glitchData.y) - offset + a_quad * newSize;
vec2 zeroToOne = pixelPos / u_resolution;
vec2 clipSpace = (zeroToOne * 2.0) - 1.0;
gl_Position = vec4(clipSpace * vec2(1.0, -1.0), 0.0, 1.0);
v_colorType = a_colorType;
}`.trim();
/**
* Фрагментный шейдер капель. Выбирает пиксель из текстурного атласа символов
* по v_uv (X внутри символа/колонки) и v_atlasRow (какая из 11 строк —
* градация яркости 0..9 или "белая голова" 10, см. buildAtlas() в
* matrix_worker.js), затем discard-ит полностью прозрачные пиксели (пустое
* пространство вокруг глифа в атласе), чтобы не тратить fill rate и не
* ломать блендинг перекрывающихся капель. Дополнительно перекрашивает
* символы рядом с курсором (v_dist из вершинного шейдера) в акцентный цвет.
*/
export const fsMatrix = `#version 300 es
precision mediump float;
in vec2 v_uv;
in float v_atlasRow;
in float v_dist;
uniform sampler2D u_atlas;
uniform float u_fontSize;
uniform vec2 u_atlasSize;
uniform vec2 u_mouse;
out vec4 outColor;
void main() {
float yOffset = v_atlasRow * u_fontSize;
vec2 finalUV = vec2(v_uv.x, v_uv.y + yOffset) / max(u_atlasSize, vec2(1.0));
vec4 texColor = texture(u_atlas, finalUV);
if (texColor.a < 0.001) discard;
if (u_mouse.x > 0.001 && u_mouse.y > 0.001 && v_dist < 0.15) {
float intensity = 1.0 - smoothstep(0.0, 0.15, v_dist);
vec3 targetColor = vec3(0.7, 0.0, 1.0);
texColor.rgb = mix(texColor.rgb, targetColor * texColor.a, intensity);
}
outColor = texColor;
}`.trim();
/**
* Фрагментный шейдер глитч-блоков. Цвет блока целиком определяется
* v_colorType (0..4), переданным из Rust (GlitchBlock::color_type,
* распределение см. в GlitchBlock::new): фон темы, основной цвет темы,
* инвертированный цвет, чистый белый и акцентный "красный сбой" —
* никакой процедурной генерации цвета здесь нет, только ветвление по типу.
*/
export const fsGlitch = `#version 300 es
precision mediump float;
in float v_colorType;
uniform vec3 u_themeMain;
uniform vec3 u_themeBg;
out vec4 outColor;
void main() {
int type = int(v_colorType + 0.1);
if (type == 0) { outColor = vec4(u_themeBg, 1.0); }
else if (type == 1) { outColor = vec4(u_themeMain, 0.9); }
else if (type == 2) { outColor = vec4(vec3(1.0) - u_themeMain, 0.7); }
else if (type == 3) { outColor = vec4(1.0, 1.0, 1.0, 0.95); }
else if (type == 4) { outColor = vec4(1.0, 0.08, 0.31, 0.95); }
else { discard; }
}`.trim();
/**
* Фоновый проход глубины: рисуется ПЕРВЫМ внутри postFramebuffer, до капель
* дождя (см. render() в matrix_worker.js) — минималистичная сцена глубины
* (небо и уходящий к линии горизонта тоннель данных: веер световых линий с
* бегущими по ним в обе стороны частицами-пакетами). Существует отдельным
* проходом, а не
* как часть постпроцесса, по одной причине: канвас матрицы создаётся с
* `alpha: false` и каждый кадр полностью перезаписывается сверху, поэтому
* любой слой, размещённый в DOM ПОЗАДИ канваса, был бы невидим — единственный
* способ дать дождю "глубину сзади" — нарисовать эту сцену первой внутри того
* же фреймбуфера, чтобы капли (которые уже блендятся поверх фона через
* SRC_ALPHA/ONE_MINUS_SRC_ALPHA) частично её перекрывали, а не полностью
* скрывали.
* Использует vsOverlay (тот же полноэкранный quad, что и постпроцесс).
* Переиспользует vsOverlay — отдельный вершинный шейдер не нужен, оба прохода
* рисуют один и тот же fullscreen-quad в clip space.
*/
export const fsBackground = `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outColor;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_isDarkMode;
uniform float u_isVpnOn;
uniform float u_isLowQuality;
float hash11(float p) { return fract(sin(p * 127.1) * 43758.5453123); }
void main() {
vec2 uv = v_uv; // 0..1, y=0 низ экрана, y=1 верх (см. vsOverlay: v_uv = a_position*0.5+0.5)
bool isLight = u_isDarkMode < 0.5;
// Акцентный цвет синхронизирован с палитрой капель/глаз: циан выкл, фиолет вкл.
vec3 accent = mix(vec3(0.0, 0.85, 1.0), vec3(0.65, 0.15, 1.0), u_isVpnOn);
// Коррекция соотношения сторон (тот же приём, что в vsMatrix/postProcessSource):
// без него линии тоннеля и частицы-пакеты искажаются при смене aspect
// ratio экрана, потому что uv.x и uv.y покрывают разное физическое
// расстояние на пиксель. Умножаем x-компоненту локальных координат на
// aspect.x перед любым distance-вычислением — тогда 1 условная единица по
// x и y соответствует одному и тому же физическому расстоянию независимо
// от формы экрана.
vec2 aspect = vec2(u_resolution.x / max(u_resolution.y, 1.0), 1.0);
float horizon = 0.46;
vec3 skyTop = isLight ? vec3(0.85, 0.87, 0.93) : vec3(0.03, 0.035, 0.06);
vec3 skyBottom = isLight ? vec3(0.90, 0.88, 0.90) : vec3(0.05, 0.04, 0.07);
vec3 col = mix(skyBottom, skyTop, smoothstep(horizon, 1.0, uv.y));
// Светящаяся полоса на линии горизонта — приглушена, чтобы не спорить с дождём.
col += accent * exp(-abs(uv.y - horizon) * 60.0) * 0.28;
if (uv.y < horizon) {
// --- Тоннель данных: минималистичная метафора вместо дороги и машин —
// веер сходящихся к горизонту световых линий (несущие "полосы"
// тоннеля), редкие поперечные кольца глубины и бегущие по линиям
// частицы-пакеты в обе стороны (входящий/исходящий трафик). Вся
// геометрия — это abs()/exp()-затухание по расстоянию до линии/точки,
// без SDF-фигур и без per-pixel шума текстуры, поэтому рендерится
// заметно дешевле прежней дороги с машинами.
float t = clamp(1.0 - uv.y / horizon, 0.0, 1.0);
// "Пол" тоннеля — лёгкое затемнение к низу экрана вместо отдельной
// текстуры асфальта.
col = mix(col, skyBottom * 0.6, t * 0.55);
// Веер несущих линий: N_LINES лучей от горизонта по той же кривой
// перспективы pow(t, 1.6), что раньше задавала края дороги.
const int N_LINES = 7;
float spread = mix(0.006, 0.42, pow(t, 1.6));
for (int li = 0; li < N_LINES; li++) {
float lineT = (float(li) / float(N_LINES - 1)) * 2.0 - 1.0;
float xLine = 0.5 + lineT * spread;
float distX = abs(uv.x - xLine) * aspect.x;
float lineWidth = mix(0.0025, 0.006, t);
col += accent * exp(-distX / lineWidth) * 0.16;
}
// Поперечные "кольца глубины" — горизонтальные полосы во всю ширину
// веера, бегущие от горизонта на зрителя (та же идея, что у прежней
// прерывистой осевой линии дороги, но во всю ширину тоннеля). При
// включённом VPN кольца бегут быстрее — метафора возросшей скорости.
float ringSpeed = mix(0.5, 1.1, u_isVpnOn);
float ringPhase = fract(t * 3.0 - u_time * ringSpeed);
float ringProximity = abs(ringPhase - 0.5) * 2.0;
float ringMask = smoothstep(0.06, 0.0, ringProximity) * smoothstep(0.0, 0.12, t);
float distFromCenterX = abs(uv.x - 0.5) * aspect.x;
if (distFromCenterX < spread * aspect.x) {
col += accent * ringMask * 0.22;
}
// --- Пакеты данных: бегут по линиям веера в обе стороны — чётные
// летят НА зрителя (входящий трафик), нечётные — ОТ зрителя
// (исходящий). При включённом VPN пакетов больше и летят быстрее —
// метафора возросшей защищённой пропускной способности. На слабых
// устройствах (u_isLowQuality) активных пакетов вдвое меньше.
float maxPackets = mix(6.0, 10.0, u_isVpnOn) * mix(1.0, 0.5, u_isLowQuality);
float speedMul = mix(1.0, 2.0, u_isVpnOn);
for (int i = 0; i < 10; i++) {
float fi = float(i);
if (fi >= maxPackets) continue;
bool incoming = mod(fi, 2.0) < 0.5;
float speed = (0.14 + hash11(fi * 7.0) * 0.06) * speedMul;
float seed = hash11(fi * 13.0) * 10.0;
float tc = fract(u_time * speed + seed);
if (!incoming) tc = 1.0 - tc;
// Пакет летит вдоль ТОЙ ЖЕ кривой веера, что и несущие линии
// выше, — визуально "едет по рельсу", а не просто где-то рядом.
float lineT = (mod(fi, float(N_LINES)) / float(N_LINES - 1)) * 2.0 - 1.0;
float pSpread = mix(0.006, 0.42, pow(tc, 1.6));
vec2 pos = vec2(0.5 + lineT * pSpread, horizon * (1.0 - tc));
vec2 d = vec2((uv.x - pos.x) * aspect.x, uv.y - pos.y);
float d2 = dot(d, d);
float size = mix(0.0025, 0.012, tc);
// Ранний выход: считаем ядро/блум пакета только рядом с ним самим.
float boundR = size * 5.0;
if (d2 > boundR * boundR) continue;
float fade = smoothstep(0.0, 0.06, tc);
vec3 packetCol = incoming ? mix(vec3(0.9, 0.97, 1.0), accent, 0.35) : accent * 0.75;
float core = (1.0 - smoothstep(size * size * 0.3, size * size, d2)) * fade;
col = mix(col, packetCol, core);
float glow = exp(-d2 / (size * size * 2.5)) * fade * (incoming ? 0.5 : 0.3);
col += packetCol * glow;
}
}
outColor = vec4(col, 1.0);
}
`.trim();
/**
* Полноэкранный фрагментный шейдер второго прохода — самый плотный файл
* пайплайна. Читает готовую картинку матрицы+глитчей из postTexture
* (см. render() в matrix_worker.js) и накладывает поверх все "объективные"
* эффекты разом: CRT-дисторсию координат, хроматическую аберрацию,
* VHS-тиры/разрывы кадра, ударную волну-рябь и процедурный рендер двух
* кибер-глаз. Два прохода (матрица в текстуру, потом пост-процесс на экран)
* нужны именно потому, что многие эффекты здесь (линзы, дисторсия, CA)
* обязаны сэмплить УЖЕ готовое изображение матрицы со смещёнными
* координатами — сделать это в один проход поверх инстансированных капель
* невозможно, т.к. у каждой капли нет доступа к соседним пикселям.
*
* Самое неочевидное:
* - crt_coords(uv, bend) — классическая barrel-дисторсия (координаты
* разъезжаются от центра к краям пропорционально offset^2), имитирует
* выпуклость старого CRT-монитора; отключается на isLowQuality для мобильных.
* - applyClassicEye() — SDF-подобная функция века: createLidCurve() строит
* верхнее/нижнее веко смесью cos-дуги и степенной кривой (pow(1-|t|^p,1/p)),
* eyeSDF = max(y - upperY, lowerY - y) — расстояние до формы глаза одним
* числом (>0 снаружи века, <=0 внутри), дальше эта дистанция переиспользуется
* и для свечения века, и для ресниц (через hash-based "шипы"), и для радужки.
* - Зрачок-"солнце": внутри радужки рисуется треугольник (правый глаз) или
* ромб (левый) через ручные SDF-выражения (dTri/dRom) — геометрическая
* заглушка вместо текстуры, чтобы не тащить лишний спрайт и держать эффект
* параметрическим (масштабируется вместе с scale/pixelScale).
* - themePrimary/themeSecondary/themeBgDark/themeAccent и caruncleArea —
* захардкоженные цвета темы и "область слёзного мясца" (внутренний уголок
* глаза, caruncleArea), дающие глазам анатомическую достоверность без
* текстур; числа синхронизированы вручную с палитрой Tailwind/темой сайта
* (see themePrimary ~ violet-900, themeAccent ~ rose-600).
* - u_vpnTimer/vpnTear — при переключении VPN на 1.0 секунды экран рвётся
* горизонтальными блоками с сдвигом (аналог VHS tracking error), давая
* ощутимый визуальный отклик на THEME-сообщение из воркера.
*/
export const postProcessSource = `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outColor;
uniform sampler2D u_mainTex;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_eyeClosedness;
uniform float u_isVpnOn;
uniform float u_eyeActive;
uniform vec2 u_eyePosL;
uniform vec2 u_eyePosR;
uniform vec2 u_gaze;
uniform vec3 u_shockwave;
uniform float u_isLowQuality;
uniform float u_distortion;
uniform float u_scanlines;
uniform float u_noise;
uniform float u_isDarkMode;
uniform float u_vpnTimer;
uniform float u_pixelScale;
float hash(vec2 p) { return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453); }
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f*f*(3.0-2.0*f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
vec2 crt_coords(vec2 uv, float bend) {
uv = uv * 2.0 - 1.0;
vec2 offset = abs(uv.yx) / vec2(bend);
uv = uv + uv * offset * offset;
return uv * 0.5 + 0.5;
}
vec2 createLidCurve(float t, float tilt, float sideSign, float closed, float powerUpper, float powerLower, float baseHeightUpper, float baseHeightLower) {
float curveU = pow(1.0 - pow(abs(t), powerUpper), 1.0/powerUpper);
float curveL = pow(1.0 - pow(abs(t), powerLower), 1.0/powerLower);
float skewedT = t - tilt * (1.0 - t*t);
float cosCurve = cos(skewedT * 1.57079632);
float verticalTilt = skewedT * 2.5 * sideSign * (1.0 - closed);
float finalUpper = mix(cosCurve, curveU, 0.4) * baseHeightUpper + verticalTilt;
float finalLower = mix(cosCurve, curveL, 0.5) * baseHeightLower - verticalTilt;
return vec2(finalUpper, finalLower);
}
void applyClassicEye(vec2 uv, vec2 eyePosNormalized, vec2 gaze, float rawClosed, bool isVpn, bool isRightEye, inout vec3 col) {
vec2 pixelPos = uv * u_resolution;
vec2 center = eyePosNormalized * u_resolution;
vec2 p = pixelPos - center;
float sideSign = isRightEye ? 1.0 : -1.0;
// ИСПРАВЛЕННЫЙ РАСЧЕТ МАСШТАБА ДЛЯ XIAOMI
float logicalWidth = u_resolution.x;
float scale = mix(0.75, 1.1, smoothstep(320.0, 1000.0, logicalWidth));
// ----------------------------------------
bool isLight = u_isDarkMode < 0.5;
vec3 themePrimary = vec3(0.3, 0.0, 0.6);
vec3 themeSecondary = vec3(0.0, 0.4, 0.5);
vec3 themeBgDark = vec3(0.043, 0.051, 0.090);
vec3 themeBgLight = vec3(0.973, 0.980, 0.988);
vec3 themeAccent = vec3(1.0, 0.0, 0.2);
vec3 colorPurple = isVpn ? vec3(0.8, 0.0, 1.0) : themePrimary;
vec3 colorCyan = isVpn ? vec3(1.0, 0.0, 0.0) : themeSecondary;
vec3 eyeScleraColor = isLight ? themeBgLight : vec3(0.02);
vec3 eyeLinerColor = isLight ? themePrimary * 0.3 : vec3(0.005);
float baseWidth = 72.0 * scale;
float baseHeightUpper = 32.0 * scale;
float baseHeightLower = 18.0 * scale;
float xNorm = clamp(p.x / baseWidth, -1.0, 1.0);
float tilt = 0.15 * sideSign;
float bounce = sin(rawClosed * 3.1415 * 2.5) * 0.05 * sin(rawClosed * 3.1415);
float closed = clamp(rawClosed - bounce, 0.0, 1.0);
vec2 curves = createLidCurve(xNorm, tilt, sideSign, closed, 2.0, 1.8, baseHeightUpper, baseHeightLower);
float meetY = mix(curves.x, -curves.y, 0.75);
float currentUpperY = mix(curves.x, meetY, closed);
float currentLowerY = mix(-curves.y, meetY, closed);
float eyeSDF = max(p.y - currentUpperY, currentLowerY - p.y);
float sdfScale = scale * mix(1.0, 0.6, closed);
float horizontalFade = 1.0 - smoothstep(0.9, 1.0, abs(xNorm));
if (horizontalFade == 0.0 || (p.x / baseWidth) < -1.0 || (p.x / baseWidth) > 1.0) return;
if (eyeSDF > 0.0) {
float glow = exp(-eyeSDF / (5.0 * sdfScale)) * horizontalFade;
if (isLight) {
col = mix(col, colorPurple, glow * (isVpn ? 0.6 : 0.25));
} else {
col += colorPurple * glow * (isVpn ? 0.7 : 0.4);
}
float linerMask = (1.0 - smoothstep(0.0, 2.5 * sdfScale, eyeSDF)) * horizontalFade;
col = mix(col, eyeLinerColor, linerMask);
float edge = (1.0 - smoothstep(0.0, 1.2 * sdfScale, eyeSDF)) * horizontalFade;
vec3 edgeColor = isLight ? themeSecondary : themeAccent;
col = mix(col, edgeColor, edge * 0.9);
float lashFreq = 22.0;
float ySign = sign(p.y - meetY);
float lashFract = fract(xNorm * lashFreq + ySign * 0.2);
float spikeShape = 1.0 - smoothstep(0.0, 0.3, abs(lashFract - 0.5));
float maxSpikeLen = (8.0 + hash(vec2(floor(xNorm * lashFreq), ySign)) * 12.0) * scale * horizontalFade;
if (isVpn) maxSpikeLen *= 1.3;
float lashMask = 1.0 - smoothstep(0.0, 1.5 * sdfScale, eyeSDF - maxSpikeLen * spikeShape);
if (lashMask > 0.0) {
vec3 lashBaseCol = isLight ? themePrimary * 0.8 : vec3(0.01);
vec3 lashTipCol = isVpn ? themeAccent : themeSecondary;
float tipGradient = smoothstep(0.0, maxSpikeLen, eyeSDF);
vec3 finalLashCol = mix(lashBaseCol, lashTipCol, tipGradient * 1.5);
col = mix(col, finalLashCol, lashMask);
}
return;
}
vec3 innerCol = eyeScleraColor;
float caruncleArea = isRightEye ? (1.0 - smoothstep(-1.0, -0.7, xNorm)) : smoothstep(0.7, 1.0, xNorm);
vec3 caruncleCol = isLight ? vec3(0.8, 0.4, 0.4) : vec3(0.15, 0.02, 0.02);
innerCol = mix(innerCol, caruncleCol, caruncleArea * smoothstep(0.0, 10.0*scale, curves.x - abs(p.y)) * 0.4);
float gridSpeed = isVpn ? u_time * 12.0 : u_time * 3.0;
float currentFullHeight = p.y > 0.0 ? abs(currentUpperY) : abs(currentLowerY);
float perspY = p.y / max(currentFullHeight, 0.1 * scale);
float gridY = fract((perspY * 10.0 * scale - gridSpeed) / (4.0 * scale));
float gridLines = smoothstep(0.85, 1.0, gridY);
float gridX = fract((p.x - (isVpn ? u_time * 15.0 : 0.0)) / (8.0 * scale));
float gridLinesX = smoothstep(0.9, 1.0, gridX);
float gridIntensity = gridLines + gridLinesX;
vec3 gridActiveColor = isLight ? themePrimary * 0.6 : colorPurple * 0.5;
float gridMix = isLight ? (gridIntensity * 0.5) : (gridIntensity * (0.2 + 0.8 * abs(perspY)));
innerCol = mix(innerCol, gridActiveColor, gridMix * (1.0 - caruncleArea * 0.8));
vec2 gazeOffset = gaze * vec2(15.0, 6.0) * scale;
vec2 pupilP = p - gazeOffset;
float r = length(pupilP);
float irisRadius = 20.0 * scale;
float irisMask = 1.0 - smoothstep(irisRadius - 1.5 * scale, irisRadius, r);
if (irisMask > 0.0) {
innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.01), irisMask);
float stripeDensity = isVpn ? 1.8 : 3.2;
float sunStripes = step(0.4, fract((pupilP.y - u_time * 4.0) / (stripeDensity * scale)));
vec3 colorBottom = isVpn ? vec3(1.0, 0.0, 0.4) : vec3(0.0, 0.7, 1.0);
vec3 colorTop = isVpn ? vec3(0.8, 0.0, 1.0) : vec3(0.6, 0.0, 1.0);
vec3 sunGradient = mix(colorBottom, colorTop, smoothstep(-irisRadius, irisRadius, pupilP.y));
float sunGlow = 1.0 - smoothstep(0.0, irisRadius, r);
vec3 sunBaseColor;
if (isLight) {
sunBaseColor = sunGradient * (sunStripes * 0.5 + 0.5);
} else {
sunBaseColor = sunGradient * (sunStripes * 0.7 + 0.3) * sunGlow * 2.0;
}
innerCol = mix(innerCol, sunBaseColor, irisMask);
float rotAngle = u_time * 1.5 * (isRightEye ? 1.0 : -1.0);
float s = sin(rotAngle), c = cos(rotAngle);
vec2 rotPupil = vec2(pupilP.x * c - pupilP.y * s, pupilP.x * s + pupilP.y * c);
float pupilRadius = isVpn ? 8.0 * scale : 5.0 * scale;
if (isRightEye) {
float dTri = max(abs(rotPupil.x) * 0.866025 + rotPupil.y * 0.5, -rotPupil.y) - pupilRadius;
float triMask = 1.0 - smoothstep(0.0, 1.5*scale, dTri);
innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.0), triMask);
vec3 triNeon = isLight ? themePrimary : colorBottom * 2.5;
innerCol = mix(innerCol, triNeon, (1.0 - smoothstep(0.0, 1.5*scale, abs(dTri))) * triMask);
} else {
float dRom = abs(rotPupil.x) + abs(rotPupil.y) - pupilRadius * 1.2;
float romMask = 1.0 - smoothstep(0.0, 1.5*scale, dRom);
innerCol = mix(innerCol, isLight ? themeBgLight : vec3(0.0), romMask);
vec3 romNeon = isLight ? themePrimary : colorTop * 2.5;
innerCol = mix(innerCol, romNeon, (1.0 - smoothstep(0.0, 1.5*scale, abs(dRom))) * romMask);
}
float specH = (1.0 - smoothstep(0.0, 1.5*scale, abs(pupilP.y - 6.0*scale))) * (1.0 - smoothstep(0.0, 9.0*scale, abs(pupilP.x + 4.5*scale)));
innerCol = mix(innerCol, vec3(1.0), specH * irisMask * (isLight ? 0.8 : 1.0));
}
if (isVpn) {
float scanY = sin(u_time * 4.0) * baseHeightUpper;
float scanMask = exp(-abs(p.y - scanY) / (1.2 * scale));
innerCol = mix(innerCol, colorCyan, scanMask * (isLight ? 0.4 : 0.6));
}
float shadowOcclusion = smoothstep(0.0, 16.0 * scale, currentUpperY - p.y);
if (p.y > -8.0*scale) {
if (isLight) {
innerCol = mix(innerCol, themePrimary * 0.2, (1.0 - shadowOcclusion) * 0.15);
} else {
innerCol *= (0.2 + 0.8 * shadowOcclusion);
}
}
col = innerCol;
}
void main() {
vec2 baseUv = (u_isLowQuality > 0.5) ? v_uv : crt_coords(v_uv, u_distortion);
bool isLight = u_isDarkMode < 0.5;
// ИСПРАВЛЕННЫЙ РАСЧЕТ МАСШТАБА ДЛЯ XIAOMI
float pScale = max(u_pixelScale, 0.1);
float logicalWidth = u_resolution.x;
float scale = mix(0.75, 1.1, smoothstep(320.0, 1000.0, logicalWidth)) * pScale;
// ----------------------------------------
vec2 lensOffset = vec2(0.0);
if (u_eyeActive > 0.5 && u_isLowQuality < 0.5) {
vec2 eL = u_eyePosL / u_resolution; eL.y = 1.0 - eL.y;
vec2 eR = u_eyePosR / u_resolution; eR.y = 1.0 - eR.y;
vec2 pL = baseUv * u_resolution - eL * u_resolution;
vec2 pR = baseUv * u_resolution - eR * u_resolution;
float rL = length(pL);
float rR = length(pR);
float lensRad = 45.0 * scale;
float refrForce = (u_isVpnOn > 0.5) ? 0.15 : 0.04;
if (rL < lensRad) {
float profile = 1.0 - smoothstep(0.0, lensRad, rL);
if (u_isVpnOn > 0.5) profile = floor(profile * 4.0) / 4.0;
lensOffset -= (pL / u_resolution) * refrForce * profile;
}
if (rR < lensRad) {
float profile = 1.0 - smoothstep(0.0, lensRad, rR);
if (u_isVpnOn > 0.5) profile = floor(profile * 4.0) / 4.0;
lensOffset -= (pR / u_resolution) * refrForce * profile;
}
}
vec2 glitchOffset = vec2(0.0);
vec2 grid = floor(baseUv * u_resolution / vec2(80.0, 40.0));
float glitchVal = hash(grid + floor(u_time * 12.0));
if (glitchVal > 0.998) glitchOffset.x += (hash(grid) - 0.5) * 0.05;
float tearHash = hash(vec2(floor(u_time * 8.0), 13.37));
float isTearing = step(0.97, tearHash);
float caOffset = 0.0;
if (isTearing > 0.5) {
float tearY = hash(vec2(floor(u_time * 8.0), 24.68));
float tearBand = 1.0 - smoothstep(0.0, 0.04, abs(baseUv.y - tearY));
glitchOffset.x += tearBand * (hash(vec2(baseUv.y * 100.0, u_time)) - 0.5) * 0.2;
caOffset = tearBand * 0.05;
}
float vpnTear = 0.0;
if (u_vpnTimer > 0.0) {
float intensity = smoothstep(0.0, 1.0, u_vpnTimer);
float tearBlock = floor(baseUv.y * 20.0 + u_time * 15.0);
float blockHash = hash(vec2(tearBlock, floor(u_time * 10.0)));
if (blockHash > 0.4) {
float shift = (hash(vec2(tearBlock, 2.0)) - 0.5) * 0.08 * intensity;
glitchOffset.x += shift;
vpnTear = intensity;
}
}
float wave = 0.0;
vec2 swDir = vec2(0.0);
if (u_shockwave.z > 0.01) {
vec2 aspect = vec2(u_resolution.x / u_resolution.y, 1.0);
vec2 centerDiff = (baseUv + glitchOffset) * aspect - u_shockwave.xy * aspect;
float baseDist = length(centerDiff);
swDir = normalize(centerDiff);
float angle = atan(swDir.y, swDir.x);
float digitalNoise = hash(vec2(floor(angle * 12.0), floor(u_time * 8.0)));
float spike = step(0.8, digitalNoise) * (digitalNoise - 0.8) * 0.4;
float wobble = noise(vec2(angle * 6.0, u_time * 5.0)) * 0.04;
float distortedDist = baseDist + spike - wobble;
float ringWidth = 0.015;
wave = (1.0 - smoothstep(u_shockwave.z, u_shockwave.z + ringWidth, distortedDist)) * smoothstep(u_shockwave.z - ringWidth, u_shockwave.z, distortedDist);
glitchOffset -= swDir * wave * 0.025;
}
vec2 bgUv = baseUv + lensOffset + glitchOffset;
vec2 eyeUv = baseUv + glitchOffset;
vec3 color = texture(u_mainTex, bgUv).rgb;
vec3 themePrimary = vec3(0.3, 0.0, 0.6);
vec3 themeSecondary = vec3(0.0, 0.4, 0.5);
vec3 themeAccent = vec3(1.0, 0.0, 0.2);
if (vpnTear > 0.0) {
color.r = texture(u_mainTex, bgUv + vec2(0.015 * vpnTear, 0.0)).r;
color.g = texture(u_mainTex, bgUv).g;
color.b = texture(u_mainTex, bgUv - vec2(0.015 * vpnTear, 0.0)).b;
float colorBand = fract(baseUv.y * 3.0 - u_time * 10.0);
vec3 glitchColor = mix(themeSecondary, themeAccent, step(0.33, colorBand));
glitchColor = mix(glitchColor, themePrimary, step(0.66, colorBand));
color = mix(color, glitchColor, vpnTear * (isLight ? 0.25 : 0.35));
color += glitchColor * vpnTear * 0.15;
}
if (caOffset > 0.0) {
color.r = texture(u_mainTex, bgUv + vec2(caOffset, 0.0)).r;
color.b = texture(u_mainTex, bgUv - vec2(caOffset, 0.0)).b;
color += (isLight ? themePrimary : vec3(0.7, 0.2, 1.0)) * caOffset * 10.0;
}
if (wave > 0.0) {
color.r = texture(u_mainTex, bgUv + swDir * wave * 0.005).r;
color.b = texture(u_mainTex, bgUv - swDir * wave * 0.005).b;
}
if (u_eyeActive > 0.5) {
vec2 eyeL = u_eyePosL / u_resolution;
vec2 eyeR = u_eyePosR / u_resolution;
eyeL.y = 1.0 - eyeL.y;
eyeR.y = 1.0 - eyeR.y;
applyClassicEye(eyeUv, eyeL, u_gaze, u_eyeClosedness, u_isVpnOn > 0.5, false, color);
applyClassicEye(eyeUv, eyeR, u_gaze, u_eyeClosedness, u_isVpnOn > 0.5, true, color);
}
if (wave > 0.0) {
vec3 shockwaveColor = (u_isVpnOn > 0.5) ? vec3(1.0, 0.0, 0.3) : vec3(0.4, 0.0, 1.0);
color += wave * 0.4 * shockwaveColor;
}
float scanline = sin(v_uv.y * u_resolution.y * 2.2) * 0.02;
color -= isLight ? scanline * 0.5 : scanline;
float vignette = clamp(pow(16.0 * v_uv.x * v_uv.y * (1.0 - v_uv.x) * (1.0 - v_uv.y), 0.25), 0.0, 1.0);
outColor = vec4(color * mix(0.9, 1.0, vignette), 1.0);
}
`.trim();