Se eliminan efectos con shaders para dejar el ejemplo unicamente de demostracion de interoperabilidad en cuda

This commit is contained in:
sebastian 2026-08-05 11:32:43 -03:00
parent 7d8368a199
commit 24b29705a7
14 changed files with 31 additions and 339 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.17) cmake_minimum_required(VERSION 3.17)
project(crt_player_cuda VERSION 1.0.0 LANGUAGES CXX) project(zero_copy_example VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -36,10 +36,10 @@ set(HEADERS
) )
# Definir ejecutable # Definir ejecutable
add_executable(crt_player_cuda ${SOURCES} ${HEADERS}) add_executable(zero_copy_example ${SOURCES} ${HEADERS})
# Enlazar librerías de Qt6, FFmpeg y CUDA # Enlazar librerías de Qt6, FFmpeg y CUDA
target_link_libraries(crt_player_cuda PRIVATE target_link_libraries(zero_copy_example PRIVATE
Qt6::Core Qt6::Core
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
@ -52,7 +52,7 @@ target_link_libraries(crt_player_cuda PRIVATE
${CUDART_LIBRARY} ${CUDART_LIBRARY}
) )
target_include_directories(crt_player_cuda PRIVATE target_include_directories(zero_copy_example PRIVATE
${AVCODEC_INCLUDE_DIRS} ${AVCODEC_INCLUDE_DIRS}
${AVFORMAT_INCLUDE_DIRS} ${AVFORMAT_INCLUDE_DIRS}
${AVUTIL_INCLUDE_DIRS} ${AVUTIL_INCLUDE_DIRS}

View file

@ -76,23 +76,25 @@ cudaMemcpy2DToArray(arrayUV, 0, 0, (void*)frame->data[1], frame->linesize[1], w,
cudaGraphicsUnmapResources(1, &m_cudaResourceY, 0); cudaGraphicsUnmapResources(1, &m_cudaResourceY, 0);
``` ```
### Punto 4: Conversión Cromática y Filtros en el Fragment Shader ### Punto 4: Conversión Cromática en el Fragment Shader
Para corregir el origen invertido entre OpenGL y CUDA, las coordenadas del Quad están configuradas con el eje vertical invertido ($V = 1.0 - V$). En el fragment shader [shader.frag](file:///home/sebastian/repos/zero_copy_video_player/shader.frag), leemos ambas texturas en paralelo y realizamos la conversión matemática de NV12 a RGB: Para corregir el origen invertido entre OpenGL y CUDA, las coordenadas del Quad están configuradas con el eje vertical invertido ($V = 1.0 - V$). En el fragment shader [shader.frag](file:///home/sebastian/repos/zero_copy_video_player/shader.frag), leemos ambas texturas en paralelo y realizamos la conversión matemática de NV12 a RGB:
```glsl ```glsl
float y = texture(u_texY, uv).r; float y = texture(u_texY, v_texCoord).r;
vec2 uvVal = texture(u_texUV, uv).rg; vec2 uvVal = texture(u_texUV, v_texCoord).rg;
// El formato NV12 almacena U y V entrelazados. Restamos 0.5 para centrar la crominancia.
float u = uvVal.r - 0.5; float u = uvVal.r - 0.5;
float v = uvVal.g - 0.5; float v = uvVal.g - 0.5;
// Matriz estándar de conversión YUV (NV12) a RGB
float r = y + 1.402 * v; float r = y + 1.402 * v;
float g = y - 0.344136 * u - 0.714136 * v; float g = y - 0.344136 * u - 0.714136 * v;
float b = y + 1.772 * u; float b = y + 1.772 * u;
vec3 color = vec3(r, g, b);
```
Por último, sobre esta señal de color RGB, se aplican los filtros de curvatura 3D de la lente, líneas de barrido (scanlines), aberración cromática y viñeteado. fragColor = vec4(r, g, b, 1.0);
```
--- ---

View file

@ -13,13 +13,7 @@ GLWidget::GLWidget(QWidget *parent)
, m_cudaResourceUV(nullptr) , m_cudaResourceUV(nullptr)
, m_allocatedWidth(0) , m_allocatedWidth(0)
, m_allocatedHeight(0) , m_allocatedHeight(0)
, m_crtEnabled(true)
, m_curvature(2.0f)
, m_scanlineDensity(400.0f)
, m_scanlineOpacity(0.35f)
, m_aberration(0.0015f)
, m_vignette(0.20f)
, m_brightness(1.10f)
{ {
m_localFrame = av_frame_alloc(); m_localFrame = av_frame_alloc();
} }
@ -51,40 +45,7 @@ void GLWidget::onFrameReady() {
update(); update();
} }
void GLWidget::setCrtEnabled(bool enabled) {
m_crtEnabled = enabled;
update();
}
void GLWidget::setCurvature(float value) {
m_curvature = value;
update();
}
void GLWidget::setScanlineDensity(float value) {
m_scanlineDensity = value;
update();
}
void GLWidget::setScanlineOpacity(float value) {
m_scanlineOpacity = value;
update();
}
void GLWidget::setAberration(float value) {
m_aberration = value;
update();
}
void GLWidget::setVignette(float value) {
m_vignette = value;
update();
}
void GLWidget::setBrightness(float value) {
m_brightness = value;
update();
}
void GLWidget::clearVideo() { void GLWidget::clearVideo() {
cleanupCudaGL(); cleanupCudaGL();
@ -227,13 +188,7 @@ void GLWidget::paintGL() {
glBindTexture(GL_TEXTURE_2D, m_texUV); glBindTexture(GL_TEXTURE_2D, m_texUV);
m_shaderProgram.setUniformValue("u_texUV", 1); m_shaderProgram.setUniformValue("u_texUV", 1);
m_shaderProgram.setUniformValue("u_crtEnabled", m_crtEnabled);
m_shaderProgram.setUniformValue("u_curvature", m_curvature);
m_shaderProgram.setUniformValue("u_scanlineDensity", m_scanlineDensity);
m_shaderProgram.setUniformValue("u_scanlineOpacity", m_scanlineOpacity);
m_shaderProgram.setUniformValue("u_aberration", m_aberration);
m_shaderProgram.setUniformValue("u_vignette", m_vignette);
m_shaderProgram.setUniformValue("u_brightness", m_brightness);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo); glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
int posLocation = m_shaderProgram.attributeLocation("a_position"); int posLocation = m_shaderProgram.attributeLocation("a_position");

View file

@ -23,14 +23,7 @@ public:
void setVideoDecoder(VideoDecoder *decoder); void setVideoDecoder(VideoDecoder *decoder);
// Ajustes del Shader CRT
void setCrtEnabled(bool enabled);
void setCurvature(float value);
void setScanlineDensity(float value);
void setScanlineOpacity(float value);
void setAberration(float value);
void setVignette(float value);
void setBrightness(float value);
void clearVideo(); void clearVideo();
@ -65,14 +58,7 @@ private:
int m_allocatedWidth; int m_allocatedWidth;
int m_allocatedHeight; int m_allocatedHeight;
// Estado del Shader
bool m_crtEnabled;
float m_curvature;
float m_scanlineDensity;
float m_scanlineOpacity;
float m_aberration;
float m_vignette;
float m_brightness;
}; };
#endif // GLWIDGET_H #endif // GLWIDGET_H

View file

@ -12,8 +12,8 @@ MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) : QMainWindow(parent)
, m_updatingSeek(false) , m_updatingSeek(false)
{ {
setWindowTitle("Retro CRT Video Player (C++/Qt6 & Native CUDA Zero-Copy)"); setWindowTitle("Zero-Copy CUDA-GL Video Player");
resize(1200, 720); resize(960, 600);
m_glWidget = new GLWidget(this); m_glWidget = new GLWidget(this);
m_videoDecoder = new VideoDecoder(this); m_videoDecoder = new VideoDecoder(this);
@ -35,7 +35,6 @@ void MainWindow::setupUI() {
this->setStyleSheet( this->setStyleSheet(
"QMainWindow { background-color: #08080f; }" "QMainWindow { background-color: #08080f; }"
"QWidget { color: #f0f0f5; font-family: 'Outfit', 'Segoe UI', sans-serif; font-size: 13px; }" "QWidget { color: #f0f0f5; font-family: 'Outfit', 'Segoe UI', sans-serif; font-size: 13px; }"
"QFrame#sidebar { background-color: #11111d; border-left: 1px solid #22223b; border-radius: 0px; }"
"QPushButton { background-color: #ff007f; color: white; border: none; padding: 8px 15px; border-radius: 6px; font-weight: bold; min-width: 60px; }" "QPushButton { background-color: #ff007f; color: white; border: none; padding: 8px 15px; border-radius: 6px; font-weight: bold; min-width: 60px; }"
"QPushButton:hover { background-color: #ff3399; }" "QPushButton:hover { background-color: #ff3399; }"
"QPushButton:pressed { background-color: #cc0066; }" "QPushButton:pressed { background-color: #cc0066; }"
@ -45,22 +44,16 @@ void MainWindow::setupUI() {
"QSlider::handle:horizontal { background: #ffffff; border: 1px solid #ff007f; width: 14px; height: 14px; margin: -4px 0; border-radius: 7px; }" "QSlider::handle:horizontal { background: #ffffff; border: 1px solid #ff007f; width: 14px; height: 14px; margin: -4px 0; border-radius: 7px; }"
"QSlider::handle:horizontal:hover { background: #ff007f; }" "QSlider::handle:horizontal:hover { background: #ff007f; }"
"QLabel { color: #a0a0c0; }" "QLabel { color: #a0a0c0; }"
"QCheckBox { spacing: 8px; }"
"QCheckBox::indicator { width: 18px; height: 18px; border: 1px solid #ff007f; border-radius: 4px; background: #1a1a2e; }"
"QCheckBox::indicator:checked { background-color: #ff007f; image: url(check.png); /* O usar un color plano */ }"
"QGroupBox { font-weight: bold; border: 1px solid #22223b; border-radius: 8px; margin-top: 15px; padding-top: 15px; color: #ff007f; }"
); );
QWidget *centralWidget = new QWidget(this); QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget); setCentralWidget(centralWidget);
QHBoxLayout *mainLayout = new QHBoxLayout(centralWidget); QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->setContentsMargins(10, 10, 10, 10); mainLayout->setContentsMargins(10, 10, 10, 10);
mainLayout->setSpacing(10); mainLayout->setSpacing(10);
// --- COLUMNA IZQUIERDA: REPRODUCTOR --- mainLayout->addWidget(m_glWidget, 1);
QVBoxLayout *playerLayout = new QVBoxLayout();
playerLayout->addWidget(m_glWidget, 1);
// Controles Multimedia // Controles Multimedia
QHBoxLayout *controlsLayout = new QHBoxLayout(); QHBoxLayout *controlsLayout = new QHBoxLayout();
@ -90,135 +83,7 @@ void MainWindow::setupUI() {
m_timeLabel->setAlignment(Qt::AlignCenter); m_timeLabel->setAlignment(Qt::AlignCenter);
controlsLayout->addWidget(m_timeLabel); controlsLayout->addWidget(m_timeLabel);
playerLayout->addLayout(controlsLayout); mainLayout->addLayout(controlsLayout);
mainLayout->addLayout(playerLayout, 3); // 3/4 del ancho de ventana
// --- COLUMNA DERECHA: SIDEBAR DE AJUSTES ---
QFrame *sidebar = new QFrame(this);
sidebar->setObjectName("sidebar");
sidebar->setFixedWidth(300);
QVBoxLayout *sidebarLayout = new QVBoxLayout(sidebar);
sidebarLayout->setContentsMargins(15, 15, 15, 15);
QLabel *sidebarTitle = new QLabel("AJUSTES CRT RETRO", this);
sidebarTitle->setStyleSheet("font-size: 16px; font-weight: bold; color: #ff007f; margin-bottom: 10px;");
sidebarTitle->setAlignment(Qt::AlignCenter);
sidebarLayout->addWidget(sidebarTitle);
QScrollArea *scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true);
scrollArea->setFrameShape(QFrame::NoFrame);
QWidget *scrollWidget = new QWidget(this);
QVBoxLayout *scrollLayout = new QVBoxLayout(scrollWidget);
scrollLayout->setContentsMargins(0, 0, 0, 0);
// Activador General
m_crtCheckbox = new QCheckBox("Habilitar Efecto CRT", this);
m_crtCheckbox->setChecked(true);
m_crtCheckbox->setStyleSheet("font-weight: bold; font-size: 14px; color: #ffffff;");
connect(m_crtCheckbox, &QCheckBox::toggled, this, &MainWindow::onCrtToggled);
scrollLayout->addWidget(m_crtCheckbox);
// Grupo de Controles Geométricos
QGroupBox *geoGroup = new QGroupBox("Geometría y Lente", this);
QVBoxLayout *geoLayout = new QVBoxLayout(geoGroup);
QLabel *curvLabel = new QLabel("Curvatura de Pantalla:", this);
geoLayout->addWidget(curvLabel);
QHBoxLayout *curvLayout = new QHBoxLayout();
m_curvatureSlider = new QSlider(Qt::Horizontal, this);
m_curvatureSlider->setRange(0, 50); // 0.0 a 5.0
m_curvatureSlider->setValue(20);
connect(m_curvatureSlider, &QSlider::valueChanged, this, &MainWindow::onCurvatureChanged);
curvLayout->addWidget(m_curvatureSlider);
m_curvatureLabel = new QLabel("2.0", this);
m_curvatureLabel->setFixedWidth(30);
curvLayout->addWidget(m_curvatureLabel);
geoLayout->addLayout(curvLayout);
scrollLayout->addWidget(geoGroup);
// Grupo de Estilo Scanline
QGroupBox *scanlineGroup = new QGroupBox("Efecto Scanlines", this);
QVBoxLayout *scanlineLayout = new QVBoxLayout(scanlineGroup);
QLabel *densLabel = new QLabel("Densidad de Líneas:", this);
scanlineLayout->addWidget(densLabel);
QHBoxLayout *densLayout = new QHBoxLayout();
m_scanlineDensitySlider = new QSlider(Qt::Horizontal, this);
m_scanlineDensitySlider->setRange(100, 1000);
m_scanlineDensitySlider->setValue(400);
connect(m_scanlineDensitySlider, &QSlider::valueChanged, this, &MainWindow::onScanlineDensityChanged);
densLayout->addWidget(m_scanlineDensitySlider);
m_scanlineDensityLabel = new QLabel("400", this);
m_scanlineDensityLabel->setFixedWidth(30);
densLayout->addWidget(m_scanlineDensityLabel);
scanlineLayout->addLayout(densLayout);
QLabel *opacLabel = new QLabel("Opacidad de Líneas:", this);
scanlineLayout->addWidget(opacLabel);
QHBoxLayout *opacLayout = new QHBoxLayout();
m_scanlineOpacitySlider = new QSlider(Qt::Horizontal, this);
m_scanlineOpacitySlider->setRange(0, 100); // 0% a 100%
m_scanlineOpacitySlider->setValue(35);
connect(m_scanlineOpacitySlider, &QSlider::valueChanged, this, &MainWindow::onScanlineOpacityChanged);
opacLayout->addWidget(m_scanlineOpacitySlider);
m_scanlineOpacityLabel = new QLabel("0.35", this);
m_scanlineOpacityLabel->setFixedWidth(30);
opacLayout->addWidget(m_scanlineOpacityLabel);
scanlineLayout->addLayout(opacLayout);
scrollLayout->addWidget(scanlineGroup);
// Grupo de Imperfecciones
QGroupBox *imperfGroup = new QGroupBox("Óptica y Color", this);
QVBoxLayout *imperfLayout = new QVBoxLayout(imperfGroup);
QLabel *aberrLabel = new QLabel("Aberración Cromática (RGB split):", this);
imperfLayout->addWidget(aberrLabel);
QHBoxLayout *aberrLayout = new QHBoxLayout();
m_aberrationSlider = new QSlider(Qt::Horizontal, this);
m_aberrationSlider->setRange(0, 100); // 0.0000 a 0.0100
m_aberrationSlider->setValue(15);
connect(m_aberrationSlider, &QSlider::valueChanged, this, &MainWindow::onAberrationChanged);
aberrLayout->addWidget(m_aberrationSlider);
m_aberrationLabel = new QLabel("0.0015", this);
m_aberrationLabel->setFixedWidth(45);
aberrLayout->addWidget(m_aberrationLabel);
imperfLayout->addLayout(aberrLayout);
QLabel *vigLabel = new QLabel("Viñeteado (Esquinas oscuras):", this);
imperfLayout->addWidget(vigLabel);
QHBoxLayout *vigLayout = new QHBoxLayout();
m_vignetteSlider = new QSlider(Qt::Horizontal, this);
m_vignetteSlider->setRange(5, 50); // 0.05 a 0.50
m_vignetteSlider->setValue(20);
connect(m_vignetteSlider, &QSlider::valueChanged, this, &MainWindow::onVignetteChanged);
vigLayout->addWidget(m_vignetteSlider);
m_vignetteLabel = new QLabel("0.20", this);
m_vignetteLabel->setFixedWidth(30);
vigLayout->addWidget(m_vignetteLabel);
imperfLayout->addLayout(vigLayout);
QLabel *brightLabel = new QLabel("Brillo Adicional:", this);
imperfLayout->addWidget(brightLabel);
QHBoxLayout *brightLayout = new QHBoxLayout();
m_brightnessSlider = new QSlider(Qt::Horizontal, this);
m_brightnessSlider->setRange(80, 150); // 0.80 a 1.50
m_brightnessSlider->setValue(110);
connect(m_brightnessSlider, &QSlider::valueChanged, this, &MainWindow::onBrightnessChanged);
brightLayout->addWidget(m_brightnessSlider);
m_brightnessLabel = new QLabel("1.10", this);
m_brightnessLabel->setFixedWidth(30);
brightLayout->addWidget(m_brightnessLabel);
imperfLayout->addLayout(brightLayout);
scrollLayout->addWidget(imperfGroup);
scrollLayout->addStretch(1);
scrollArea->setWidget(scrollWidget);
sidebarLayout->addWidget(scrollArea);
mainLayout->addWidget(sidebar);
} }
void MainWindow::openVideoFile() { void MainWindow::openVideoFile() {
@ -289,43 +154,4 @@ void MainWindow::onPlaybackFinished() {
m_seekBar->setValue(0); m_seekBar->setValue(0);
} }
// --- SLOTS DE PARÁMETROS DEL SHADER ---
void MainWindow::onCrtToggled(bool checked) {
m_glWidget->setCrtEnabled(checked);
}
void MainWindow::onCurvatureChanged(int value) {
float curvatureVal = value / 10.0f;
m_curvatureLabel->setText(QString::number(curvatureVal, 'f', 1));
m_glWidget->setCurvature(curvatureVal);
}
void MainWindow::onScanlineDensityChanged(int value) {
m_scanlineDensityLabel->setText(QString::number(value));
m_glWidget->setScanlineDensity(static_cast<float>(value));
}
void MainWindow::onScanlineOpacityChanged(int value) {
float opacityVal = value / 100.0f;
m_scanlineOpacityLabel->setText(QString::number(opacityVal, 'f', 2));
m_glWidget->setScanlineOpacity(opacityVal);
}
void MainWindow::onAberrationChanged(int value) {
float aberrationVal = value / 10000.0f;
m_aberrationLabel->setText(QString::number(aberrationVal, 'f', 4));
m_glWidget->setAberration(aberrationVal);
}
void MainWindow::onVignetteChanged(int value) {
float vignetteVal = value / 100.0f;
m_vignetteLabel->setText(QString::number(vignetteVal, 'f', 2));
m_glWidget->setVignette(vignetteVal);
}
void MainWindow::onBrightnessChanged(int value) {
float brightnessVal = value / 100.0f;
m_brightnessLabel->setText(QString::number(brightnessVal, 'f', 2));
m_glWidget->setBrightness(brightnessVal);
}

View file

@ -24,14 +24,7 @@ private slots:
void onPositionChanged(qint64 positionMs); void onPositionChanged(qint64 positionMs);
void onPlaybackFinished(); void onPlaybackFinished();
// Slots de actualización de los sliders del Shader
void onCrtToggled(bool checked);
void onCurvatureChanged(int value);
void onScanlineDensityChanged(int value);
void onScanlineOpacityChanged(int value);
void onAberrationChanged(int value);
void onVignetteChanged(int value);
void onBrightnessChanged(int value);
private: private:
void setupUI(); void setupUI();
@ -46,20 +39,7 @@ private:
QSlider *m_seekBar; QSlider *m_seekBar;
QLabel *m_timeLabel; QLabel *m_timeLabel;
// Sliders del Shader y sus etiquetas indicadoras
QCheckBox *m_crtCheckbox;
QSlider *m_curvatureSlider;
QLabel *m_curvatureLabel;
QSlider *m_scanlineDensitySlider;
QLabel *m_scanlineDensityLabel;
QSlider *m_scanlineOpacitySlider;
QLabel *m_scanlineOpacityLabel;
QSlider *m_aberrationSlider;
QLabel *m_aberrationLabel;
QSlider *m_vignetteSlider;
QLabel *m_vignetteLabel;
QSlider *m_brightnessSlider;
QLabel *m_brightnessLabel;
}; };
#endif // MAINWINDOW_H #endif // MAINWINDOW_H

View file

@ -5,76 +5,19 @@ out vec4 fragColor;
uniform sampler2D u_texY; uniform sampler2D u_texY;
uniform sampler2D u_texUV; uniform sampler2D u_texUV;
uniform bool u_crtEnabled;
uniform float u_curvature;
uniform float u_scanlineDensity;
uniform float u_scanlineOpacity;
uniform float u_aberration;
uniform float u_vignette;
uniform float u_brightness;
uniform float u_time;
// Función para distorsión de barril (curvatura CRT) void main() {
vec2 curve(vec2 uv) { float y = texture(u_texY, v_texCoord).r;
if (u_curvature <= 0.0) return uv; vec2 uvVal = texture(u_texUV, v_texCoord).rg;
uv = (uv - 0.5) * 2.0;
uv.x *= 1.0 + (uv.y * uv.y) * (u_curvature * 0.05); // El formato NV12 almacena U y V entrelazados. Restamos 0.5 para centrar la crominancia.
uv.y *= 1.0 + (uv.x * uv.x) * (u_curvature * 0.05);
uv = (uv / 2.0) + 0.5;
return uv;
}
// Conversión NV12 (Y + UV entrelazados) a RGB
vec3 getNv12Color(vec2 uv) {
float y = texture(u_texY, uv).r;
vec2 uvVal = texture(u_texUV, uv).rg;
float u = uvVal.r - 0.5; float u = uvVal.r - 0.5;
float v = uvVal.g - 0.5; float v = uvVal.g - 0.5;
// Matriz estándar de conversión YUV (NV12) a RGB
float r = y + 1.402 * v; float r = y + 1.402 * v;
float g = y - 0.344136 * u - 0.714136 * v; float g = y - 0.344136 * u - 0.714136 * v;
float b = y + 1.772 * u; float b = y + 1.772 * u;
return vec3(r, g, b);
} fragColor = vec4(r, g, b, 1.0);
void main() {
vec2 uv = v_texCoord;
if (u_crtEnabled) {
uv = curve(uv);
}
// Verificar límites para recortar esquinas curvas
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
vec3 color;
if (u_crtEnabled && u_aberration > 0.0) {
// Aberración Cromática (RGB split) utilizando muestreos desfasados de NV12
float r = getNv12Color(vec2(uv.x - u_aberration, uv.y)).r;
float g = getNv12Color(uv).g;
float b = getNv12Color(vec2(uv.x + u_aberration, uv.y)).b;
color = vec3(r, g, b);
} else {
color = getNv12Color(uv);
}
// Aplicar Brillo
color *= u_brightness;
if (u_crtEnabled) {
// 1. Efecto Scanlines (Líneas de Barrido)
float scanline = sin(uv.y * u_scanlineDensity * 3.14159265);
scanline = mix(1.0, (scanline + 1.0) * 0.5, u_scanlineOpacity);
color *= scanline;
// 2. Efecto Viñeteado (Esquinas oscuras)
float vignette = uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y);
vignette = clamp(pow(16.0 * vignette, u_vignette), 0.0, 1.0);
color *= vignette;
}
fragColor = vec4(color, 1.0);
} }