optimization and stylizing
This commit is contained in:
@@ -74,7 +74,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
|
|||||||
const isLowQuality = isMobile || isWeakHardware;
|
const isLowQuality = isMobile || isWeakHardware;
|
||||||
|
|
||||||
// На слабом ПК и телефонах рендерим 75% от разрешения экрана
|
// На слабом ПК и телефонах рендерим 75% от разрешения экрана
|
||||||
const pixelScale = isLowQuality ? 0.75 : 1.0;
|
const pixelScale = 1;
|
||||||
// ----------------------------------------------
|
// ----------------------------------------------
|
||||||
|
|
||||||
const logicalWidth = container.clientWidth;
|
const logicalWidth = container.clientWidth;
|
||||||
@@ -127,7 +127,7 @@ export const NetrunnerMatrix: React.FC<NetrunnerMatrixProps> = ({
|
|||||||
|
|
||||||
const isMob = width <= 768;
|
const isMob = width <= 768;
|
||||||
const lowQual = isMob || isWeakHardware;
|
const lowQual = isMob || isWeakHardware;
|
||||||
const pScale = lowQual ? 0.75 : 1.0;
|
const pScale = 1;
|
||||||
|
|
||||||
workerRef.current?.postMessage({
|
workerRef.current?.postMessage({
|
||||||
type: "RESIZE",
|
type: "RESIZE",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ let pixelScale = 1.0;
|
|||||||
let isLowQuality = false;
|
let isLowQuality = false;
|
||||||
let frameCounter = 0;
|
let frameCounter = 0;
|
||||||
let eyeScale = 1.5;
|
let eyeScale = 1.5;
|
||||||
|
let interactionTimer = 0;
|
||||||
|
|
||||||
let postProgram;
|
let postProgram;
|
||||||
let postFramebuffer, postTexture;
|
let postFramebuffer, postTexture;
|
||||||
@@ -133,9 +134,12 @@ float hash21(vec2 p) {
|
|||||||
void main() {
|
void main() {
|
||||||
vec2 crt_uv = crt_coords(v_uv, u_distortion);
|
vec2 crt_uv = crt_coords(v_uv, u_distortion);
|
||||||
|
|
||||||
|
// Цвет фона: жестко привязываем к теме
|
||||||
|
vec3 current_bg = u_isDarkMode > 0.5 ? vec3(10.0/255.0, 10.0/255.0, 12.0/255.0) : vec3(250.0/255.0, 250.0/255.0, 252.0/255.0);
|
||||||
|
|
||||||
|
// Если вышли за границы "пузатого" монитора
|
||||||
if (crt_uv.x < 0.0 || crt_uv.x > 1.0 || crt_uv.y < 0.0 || crt_uv.y > 1.0) {
|
if (crt_uv.x < 0.0 || crt_uv.x > 1.0 || crt_uv.y < 0.0 || crt_uv.y > 1.0) {
|
||||||
float bg = u_isDarkMode > 0.5 ? 0.0 : 0.98;
|
outColor = vec4(current_bg, 1.0); // Теперь тут будет белый в светлой теме
|
||||||
outColor = vec4(bg, bg, bg, 1.0);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +311,8 @@ function initWebGL() {
|
|||||||
function buildAtlas() {
|
function buildAtlas() {
|
||||||
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 11);
|
atlasCanvas = new OffscreenCanvas(chars.length * fontSize, fontSize * 11);
|
||||||
atlasCtx = atlasCanvas.getContext("2d", { alpha: true });
|
atlasCtx = atlasCanvas.getContext("2d", { alpha: true });
|
||||||
|
|
||||||
|
// 1. Полная очистка
|
||||||
atlasCtx.clearRect(0, 0, atlasCanvas.width, atlasCanvas.height);
|
atlasCtx.clearRect(0, 0, atlasCanvas.width, atlasCanvas.height);
|
||||||
|
|
||||||
let r, g, b;
|
let r, g, b;
|
||||||
@@ -333,11 +339,10 @@ function buildAtlas() {
|
|||||||
atlasCtx.textAlign = "center";
|
atlasCtx.textAlign = "center";
|
||||||
const halfFs = fontSize / 2;
|
const halfFs = fontSize / 2;
|
||||||
|
|
||||||
|
// 2. Рисуем ТОЛЬКО текст. Никаких fillRect под ним!
|
||||||
for (let row = 0; row <= 9; row++) {
|
for (let row = 0; row <= 9; row++) {
|
||||||
const alpha = (row + 1) / 10;
|
const alpha = isLowQuality ? 1.0 : (row + 1) / 10;
|
||||||
const yOffset = row * fontSize;
|
const yOffset = row * fontSize;
|
||||||
|
|
||||||
// УБРАЛИ fillRect с серым фоном. Оставляем ТОЛЬКО цветной текст!
|
|
||||||
atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
atlasCtx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
for (let i = 0; i < chars.length; i++) {
|
for (let i = 0; i < chars.length; i++) {
|
||||||
atlasCtx.fillText(chars[i], i * fontSize + halfFs, yOffset);
|
atlasCtx.fillText(chars[i], i * fontSize + halfFs, yOffset);
|
||||||
@@ -345,7 +350,6 @@ function buildAtlas() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headY = 10 * fontSize;
|
const headY = 10 * fontSize;
|
||||||
// УБРАЛИ fillRect для "головы"
|
|
||||||
atlasCtx.fillStyle = "rgb(255, 255, 255)";
|
atlasCtx.fillStyle = "rgb(255, 255, 255)";
|
||||||
for (let i = 0; i < chars.length; i++) {
|
for (let i = 0; i < chars.length; i++) {
|
||||||
atlasCtx.fillText(chars[i], i * fontSize + halfFs, headY);
|
atlasCtx.fillText(chars[i], i * fontSize + halfFs, headY);
|
||||||
@@ -363,11 +367,8 @@ function buildAtlas() {
|
|||||||
);
|
);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ОТРИСОВКИ ---
|
// --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ОТРИСОВКИ ---
|
||||||
function getGlitchColor(type) {
|
function getGlitchColor(type) {
|
||||||
let r, g, b;
|
let r, g, b;
|
||||||
@@ -573,13 +574,26 @@ function drawOverlayElements() {
|
|||||||
const eyeLen = engine.eye_len();
|
const eyeLen = engine.eye_len();
|
||||||
if (eyeLen >= 6) {
|
if (eyeLen >= 6) {
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
|
interactionTimer = 120;
|
||||||
|
// ОПТИМИЗАЦИЯ ДЛЯ СЛАБЫХ УСТРОЙСТВ
|
||||||
|
// На старом ноуте/Xiaomi обновляем тяжелую текстуру глаз в 2 раза реже
|
||||||
|
if (isLowQuality && frameCounter % 2 !== 0) {
|
||||||
|
// Пропускаем очистку и отрисовку в этом кадре,
|
||||||
|
// WebGL просто выведет старую текстуру из кэша
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
overlayCtx.globalAlpha = 1.0;
|
overlayCtx.globalAlpha = 1.0;
|
||||||
const eyeData = new Float32Array(
|
const eyeData = new Float32Array(
|
||||||
wasm.memory.buffer,
|
wasm.memory.buffer,
|
||||||
engine.eye_ptr(),
|
engine.eye_ptr(),
|
||||||
eyeLen,
|
eyeLen,
|
||||||
);
|
);
|
||||||
const gap = isMobile ? 85 * eyeScale : 85 * eyeScale;
|
|
||||||
|
// Исправленный gap: на мобилках глаза ближе друг к другу (60 вместо 85)
|
||||||
|
const gap = (isMobile ? 75.0 : 80.0) * eyeScale;
|
||||||
|
|
||||||
|
// Левый глаз
|
||||||
drawEye(
|
drawEye(
|
||||||
overlayCtx,
|
overlayCtx,
|
||||||
eyeData[0] - gap,
|
eyeData[0] - gap,
|
||||||
@@ -589,6 +603,8 @@ function drawOverlayElements() {
|
|||||||
eyeData[4],
|
eyeData[4],
|
||||||
eyeData[5],
|
eyeData[5],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Правый глаз
|
||||||
drawEye(
|
drawEye(
|
||||||
overlayCtx,
|
overlayCtx,
|
||||||
eyeData[0] + gap,
|
eyeData[0] + gap,
|
||||||
@@ -598,11 +614,11 @@ function drawOverlayElements() {
|
|||||||
eyeData[4],
|
eyeData[4],
|
||||||
eyeData[5],
|
eyeData[5],
|
||||||
);
|
);
|
||||||
|
} else if (interactionTimer > 0) {
|
||||||
|
interactionTimer--;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasContent;
|
return hasContent;
|
||||||
}
|
}
|
||||||
// --- ГЛАВНЫЙ ЦИКЛ ОТРИСОВКИ ---
|
|
||||||
function render(currentTime) {
|
function render(currentTime) {
|
||||||
requestAnimationFrame(render);
|
requestAnimationFrame(render);
|
||||||
if (!engine || !wasm || !gl) return;
|
if (!engine || !wasm || !gl) return;
|
||||||
@@ -611,27 +627,23 @@ function render(currentTime) {
|
|||||||
if (now - lastDrawTime < 16) return;
|
if (now - lastDrawTime < 16) return;
|
||||||
lastDrawTime = now;
|
lastDrawTime = now;
|
||||||
|
|
||||||
if (isLowQuality) {
|
// 1. Физика капель (WASM)
|
||||||
frameCounter++;
|
// Для слабых устройств можно включить троттлинг обратно, если будет греться
|
||||||
if (frameCounter % 2 === 0) {
|
engine.tick();
|
||||||
engine.tick();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
engine.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ШАГ 1: РЕНДЕРИМ ВСЁ В ТЕКСТУРУ (FRAMEBUFFER) ---
|
const targetBuffer = isLowQuality ? null : postFramebuffer;
|
||||||
// Направляем вывод WebGL в буфер постобработки, а не на экран
|
gl.bindFramebuffer(gl.FRAMEBUFFER, targetBuffer);
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, postFramebuffer);
|
|
||||||
gl.viewport(0, 0, width * pixelScale, height * pixelScale);
|
gl.viewport(0, 0, width * pixelScale, height * pixelScale);
|
||||||
|
|
||||||
const bgR = isDarkMode ? 10 / 255 : 250 / 255;
|
const bgR = isDarkMode ? 10 / 255 : 250 / 255;
|
||||||
const bgG = isDarkMode ? 10 / 255 : 250 / 255;
|
const bgG = isDarkMode ? 10 / 255 : 250 / 255;
|
||||||
const bgB = isDarkMode ? 12 / 255 : 252 / 255;
|
const bgB = isDarkMode ? 12 / 255 : 252 / 255;
|
||||||
|
|
||||||
|
// Всегда очищаем реальным цветом фона, а не прозрачностью!
|
||||||
|
// Это гарантирует, что даже если шейдер заглючит, под ним будет верный цвет.
|
||||||
gl.clearColor(bgR, bgG, bgB, 1.0);
|
gl.clearColor(bgR, bgG, bgB, 1.0);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
// 1.1. РЕНДЕР МАТРИЦЫ (Instanced Rendering)
|
||||||
// 1.1. РЕНДЕР МАТРИЦЫ
|
|
||||||
const renderLen = engine.render_len();
|
const renderLen = engine.render_len();
|
||||||
const instanceCount = renderLen / 5;
|
const instanceCount = renderLen / 5;
|
||||||
if (instanceCount > 0) {
|
if (instanceCount > 0) {
|
||||||
@@ -663,42 +675,53 @@ function render(currentTime) {
|
|||||||
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
|
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.2. РЕНДЕР ОВЕРЛЕЯ (Глаза, глитчи)
|
// 1.2. РЕНДЕР ОВЕРЛЕЯ (Глаза и эффекты)
|
||||||
if (drawOverlayElements()) {
|
const needsOverlayUpdate = drawOverlayElements();
|
||||||
|
|
||||||
|
// КРИТИЧЕСКАЯ ОПТИМИЗАЦИЯ ДЛЯ СЛАБЫХ ПК:
|
||||||
|
// Не вызываем рендер оверлея, если он статичен (interactionTimer вышел)
|
||||||
|
const skipOverlay =
|
||||||
|
isLowQuality && interactionTimer <= 0 && engine.glitch_len() === 0;
|
||||||
|
|
||||||
|
if (!skipOverlay) {
|
||||||
gl.useProgram(overlayProgram);
|
gl.useProgram(overlayProgram);
|
||||||
gl.bindVertexArray(vaoOverlay);
|
gl.bindVertexArray(vaoOverlay);
|
||||||
gl.activeTexture(gl.TEXTURE1);
|
gl.activeTexture(gl.TEXTURE1);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, overlayTexture);
|
gl.bindTexture(gl.TEXTURE_2D, overlayTexture);
|
||||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
||||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
if (needsOverlayUpdate) {
|
||||||
gl.texImage2D(
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
||||||
gl.TEXTURE_2D,
|
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
|
||||||
0,
|
gl.texImage2D(
|
||||||
gl.RGBA,
|
gl.TEXTURE_2D,
|
||||||
gl.RGBA,
|
0,
|
||||||
gl.UNSIGNED_BYTE,
|
gl.RGBA,
|
||||||
overlayCanvas,
|
gl.RGBA,
|
||||||
);
|
gl.UNSIGNED_BYTE,
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
overlayCanvas,
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
}
|
||||||
|
|
||||||
gl.uniform1i(gl.getUniformLocation(overlayProgram, "u_texture"), 1);
|
gl.uniform1i(gl.getUniformLocation(overlayProgram, "u_texture"), 1);
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ШАГ 2: ПОСТОБРАБОТКА (ВЫВОД НА ЭКРАН) ---
|
// 3. ФИНАЛЬНЫЙ ЭТАП (ПОСТОБРАБОТКА)
|
||||||
// Возвращаемся к стандартному буферу (экран)
|
// Если мы на слабом устройстве — работа закончена, выходим!
|
||||||
|
if (isLowQuality) return;
|
||||||
|
|
||||||
|
// Для мощных ПК переносим результат из скрытого буфера на экран с эффектами
|
||||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||||
|
gl.useProgram(postProgram);
|
||||||
|
gl.bindVertexArray(vaoOverlay);
|
||||||
|
|
||||||
gl.useProgram(postProgram); // <--- ИСПОЛЬЗУЕМ ВАШ НОВЫЙ ШЕЙДЕР
|
|
||||||
gl.bindVertexArray(vaoOverlay); // Переиспользуем квадрат на весь экран
|
|
||||||
|
|
||||||
// Передаем текстуру, которую только что нарисовали (Шаг 1)
|
|
||||||
gl.activeTexture(gl.TEXTURE2);
|
gl.activeTexture(gl.TEXTURE2);
|
||||||
gl.bindTexture(gl.TEXTURE_2D, postTexture);
|
gl.bindTexture(gl.TEXTURE_2D, postTexture);
|
||||||
gl.uniform1i(gl.getUniformLocation(postProgram, "u_mainTex"), 2);
|
gl.uniform1i(gl.getUniformLocation(postProgram, "u_mainTex"), 2);
|
||||||
|
|
||||||
// Передаем параметры эффектов
|
|
||||||
gl.uniform1f(gl.getUniformLocation(postProgram, "u_time"), now / 1000);
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_time"), now / 1000);
|
||||||
gl.uniform2f(
|
gl.uniform2f(
|
||||||
gl.getUniformLocation(postProgram, "u_resolution"),
|
gl.getUniformLocation(postProgram, "u_resolution"),
|
||||||
@@ -706,23 +729,20 @@ function render(currentTime) {
|
|||||||
height * pixelScale,
|
height * pixelScale,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Значения: Сбалансированный киберпанк
|
// Параметры "Киберпанк 1984"
|
||||||
gl.uniform1f(gl.getUniformLocation(postProgram, "u_distortion"), 18.0); // Изгиб оставляем как есть
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_distortion"), 18.0);
|
||||||
gl.uniform1f(gl.getUniformLocation(postProgram, "u_aberration"), 0.008); // Уменьшили "радугу" по краям в два раза
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_aberration"), 0.008);
|
||||||
gl.uniform1f(gl.getUniformLocation(postProgram, "u_scanlines"), 0.15); // Сделали полосы тоньше и прозрачнее
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_scanlines"), 0.15);
|
||||||
gl.uniform1f(gl.getUniformLocation(postProgram, "u_noise"), 0.05); // Чуть убавили цифровой шум
|
gl.uniform1f(gl.getUniformLocation(postProgram, "u_noise"), 0.05);
|
||||||
|
|
||||||
gl.uniform1f(
|
gl.uniform1f(
|
||||||
gl.getUniformLocation(postProgram, "u_glitch_intensity"),
|
gl.getUniformLocation(postProgram, "u_glitch_intensity"),
|
||||||
isVpnOn ? 0.3 : 0.0,
|
isVpnOn ? 0.3 : 0.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
gl.uniform1f(
|
gl.uniform1f(
|
||||||
gl.getUniformLocation(postProgram, "u_isDarkMode"),
|
gl.getUniformLocation(postProgram, "u_isDarkMode"),
|
||||||
isDarkMode ? 1.0 : 0.0,
|
isDarkMode ? 1.0 : 0.0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Рисуем финальный прямоугольник с эффектами
|
|
||||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ impl MatrixEngine {
|
|||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new(width: u32, height: u32, font_size: u32) -> MatrixEngine {
|
pub fn new(width: u32, height: u32, font_size: u32) -> MatrixEngine {
|
||||||
console_error_panic_hook::set_once();
|
console_error_panic_hook::set_once();
|
||||||
let max_drops = 10_000;
|
let max_drops = 250;
|
||||||
let shockwaves = (0..5)
|
let shockwaves = (0..5)
|
||||||
.map(|_| Shockwave {
|
.map(|_| Shockwave {
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user