/** * Файл содержит все исходные коды шейдеров для проекта 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();