Implementar shader CRT con post-processing: migración completa GLSL a MSL

- Migrar shader CRT de GLSL a Metal Shading Language (MSL)
- Implementar renderizado de dos pasadas:
  * Paso 1: Renderizar escena a textura offscreen
  * Paso 2: Aplicar efecto CRT como post-procesamiento
- Añadir shaders CRT con scanlines, shadow mask y gamma correction
- Crear offscreen render target para renderizado intermedio
- Implementar fullscreen quad para post-procesamiento
- Configurar pipeline CRT con samplers linear y NEAREST
- Mantener compatibilidad con sprites multi-color existentes
- Resolución virtual CRT: 320x240 para apariencia retro auténtica

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-28 18:30:01 +02:00
parent 62dee3b2bb
commit 909635d76d
2 changed files with 568 additions and 45 deletions

View File

@@ -38,6 +38,7 @@
// Estructura para vértices de sprites
struct SpriteVertex {
float position[2]; // x, y coordinates
float color[4]; // r, g, b, a color components
float texCoord[2]; // u, v texture coordinates
};
@@ -204,24 +205,27 @@ fragment float4 triangle_fragment_main(VertexOut in [[stage_in]]) {
}
)";
// Shaders para sprites con textura
// Shaders para sprites con textura y color
NSString* spriteVertexShaderSource = @R"(
#include <metal_stdlib>
using namespace metal;
struct SpriteVertexIn {
float2 position [[attribute(0)]];
float2 texCoord [[attribute(1)]];
float4 color [[attribute(1)]];
float2 texCoord [[attribute(2)]];
};
struct SpriteVertexOut {
float4 position [[position]];
float4 color;
float2 texCoord;
};
vertex SpriteVertexOut sprite_vertex_main(SpriteVertexIn in [[stage_in]]) {
SpriteVertexOut out;
out.position = float4(in.position, 0.0, 1.0);
out.color = in.color;
out.texCoord = in.texCoord;
return out;
}
@@ -233,6 +237,7 @@ using namespace metal;
struct SpriteVertexOut {
float4 position [[position]];
float4 color;
float2 texCoord;
};
@@ -240,12 +245,205 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
texture2d<float> spriteTexture [[texture(0)]],
sampler textureSampler [[sampler(0)]]) {
float4 textureColor = spriteTexture.sample(textureSampler, in.texCoord);
return textureColor;
return textureColor * in.color; // Multiplicar textura por color de vértice para tinting
}
)";
// Compilar shaders de fondo
// Shaders para CRT post-processing (fullscreen quad)
NSString* crtVertexShaderSource = @R"(
#include <metal_stdlib>
using namespace metal;
struct CRTVertexOut {
float4 position [[position]];
float2 texCoord;
};
vertex CRTVertexOut crt_vertex_main(uint vertexID [[vertex_id]]) {
CRTVertexOut out;
// Fullscreen quad con coordenadas de textura
float2 positions[6] = {
float2(-1.0, -1.0), // Bottom left
float2( 1.0, -1.0), // Bottom right
float2(-1.0, 1.0), // Top left
float2( 1.0, -1.0), // Bottom right
float2( 1.0, 1.0), // Top right
float2(-1.0, 1.0) // Top left
};
float2 texCoords[6] = {
float2(0.0, 1.0), // Bottom left
float2(1.0, 1.0), // Bottom right
float2(0.0, 0.0), // Top left
float2(1.0, 1.0), // Bottom right
float2(1.0, 0.0), // Top right
float2(0.0, 0.0) // Top left
};
out.position = float4(positions[vertexID], 0.0, 1.0);
out.texCoord = texCoords[vertexID];
return out;
}
)";
// CRT Fragment Shader - Migrado de GLSL a MSL
NSString* crtFragmentShaderSource = @R"(
#include <metal_stdlib>
using namespace metal;
// Parámetros del CRT shader
#define CURVATURE_X 0.05
#define CURVATURE_Y 0.1
#define MASK_BRIGHTNESS 0.80
#define SCANLINE_WEIGHT 6.0
#define SCANLINE_GAP_BRIGHTNESS 0.12
#define BLOOM_FACTOR 3.5
#define INPUT_GAMMA 2.4
#define OUTPUT_GAMMA 2.2
// Features habilitadas
#define SCANLINES
#define MULTISAMPLE
#define GAMMA
// #define FAKE_GAMMA
// #define CURVATURE
// #define SHARPER
#define MASK_TYPE 2
struct CRTVertexOut {
float4 position [[position]];
float2 texCoord;
};
float CalcScanLineWeight(float dist) {
return max(1.0 - dist * dist * SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS);
}
float CalcScanLine(float dy, float filterWidth) {
float scanLineWeight = CalcScanLineWeight(dy);
#ifdef MULTISAMPLE
scanLineWeight += CalcScanLineWeight(dy - filterWidth);
scanLineWeight += CalcScanLineWeight(dy + filterWidth);
scanLineWeight *= 0.3333333;
#endif
return scanLineWeight;
}
fragment float4 crt_fragment_main(CRTVertexOut in [[stage_in]],
texture2d<float> sceneTexture [[texture(0)]],
sampler sceneSampler [[sampler(0)]]) {
float2 TextureSize = float2(320.0, 240.0); // Resolución virtual del CRT
float filterWidth = (768.0 / 240.0) / 3.0;
float2 texcoord = in.texCoord;
// Convertir a píxeles
float2 texcoordInPixels = texcoord * TextureSize;
#ifdef SHARPER
float2 tempCoord = floor(texcoordInPixels) + 0.5;
float2 coord = tempCoord / TextureSize;
float2 deltas = texcoordInPixels - tempCoord;
float scanLineWeight = CalcScanLine(deltas.y, filterWidth);
float2 signs = sign(deltas);
deltas.x *= 2.0;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5;
deltas.y *= 8.0;
deltas /= TextureSize;
deltas *= signs;
float2 tc = coord + deltas;
#else
float tempY = floor(texcoordInPixels.y) + 0.5;
float yCoord = tempY / TextureSize.y;
float dy = texcoordInPixels.y - tempY;
float scanLineWeight = CalcScanLine(dy, filterWidth);
float signY = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0;
dy /= TextureSize.y;
dy *= signY;
float2 tc = float2(texcoord.x, yCoord + dy);
#endif
// Samplear la textura de escena
float3 colour = sceneTexture.sample(sceneSampler, tc).rgb;
#ifdef SCANLINES
#ifdef GAMMA
#ifdef FAKE_GAMMA
colour = colour * colour;
#else
colour = pow(colour, float3(INPUT_GAMMA));
#endif
#endif
scanLineWeight *= BLOOM_FACTOR;
colour *= scanLineWeight;
#ifdef GAMMA
#ifdef FAKE_GAMMA
colour = sqrt(colour);
#else
colour = pow(colour, float3(1.0 / OUTPUT_GAMMA));
#endif
#endif
#endif
// Shadow mask
#if MASK_TYPE == 0
return float4(colour, 1.0);
#else
#if MASK_TYPE == 1
float whichMask = fract((in.position.x * 1.0001) * 0.5);
float3 mask;
if (whichMask < 0.5) {
mask = float3(MASK_BRIGHTNESS, 1.0, MASK_BRIGHTNESS);
} else {
mask = float3(1.0, MASK_BRIGHTNESS, 1.0);
}
#elif MASK_TYPE == 2
float whichMask = fract((in.position.x * 1.0001) * 0.3333333);
float3 mask = float3(MASK_BRIGHTNESS, MASK_BRIGHTNESS, MASK_BRIGHTNESS);
if (whichMask < 0.3333333) {
mask.x = 1.0;
} else if (whichMask < 0.6666666) {
mask.y = 1.0;
} else {
mask.z = 1.0;
}
#endif
return float4(colour * mask, 1.0);
#endif
}
)";
// Compilar shaders CRT
NSError* error = nil;
id<MTLLibrary> crtVertexLibrary = [device newLibraryWithSource:crtVertexShaderSource options:nil error:&error];
if (!crtVertexLibrary || error) {
if (error) {
std::cout << "Failed to compile CRT vertex shader: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
id<MTLLibrary> crtFragmentLibrary = [device newLibraryWithSource:crtFragmentShaderSource options:nil error:&error];
if (!crtFragmentLibrary || error) {
if (error) {
std::cout << "Failed to compile CRT fragment shader: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
id<MTLFunction> crtVertexFunction = [crtVertexLibrary newFunctionWithName:@"crt_vertex_main"];
id<MTLFunction> crtFragmentFunction = [crtFragmentLibrary newFunctionWithName:@"crt_fragment_main"];
// Compilar shaders de fondo
id<MTLLibrary> backgroundVertexLibrary = [device newLibraryWithSource:backgroundVertexShaderSource options:nil error:&error];
if (!backgroundVertexLibrary || error) {
if (error) {
@@ -348,14 +546,17 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
spritePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
spritePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
// Configurar vertex descriptor para sprites
// Configurar vertex descriptor para sprites (position + color + texCoord)
MTLVertexDescriptor* spriteVertexDescriptor = [[MTLVertexDescriptor alloc] init];
spriteVertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
spriteVertexDescriptor.attributes[0].offset = 0;
spriteVertexDescriptor.attributes[0].bufferIndex = 0;
spriteVertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoord
spriteVertexDescriptor.attributes[1].format = MTLVertexFormatFloat4; // color
spriteVertexDescriptor.attributes[1].offset = 8;
spriteVertexDescriptor.attributes[1].bufferIndex = 0;
spriteVertexDescriptor.attributes[2].format = MTLVertexFormatFloat2; // texCoord
spriteVertexDescriptor.attributes[2].offset = 24;
spriteVertexDescriptor.attributes[2].bufferIndex = 0;
spriteVertexDescriptor.layouts[0].stride = sizeof(SpriteVertex);
spriteVertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
@@ -417,15 +618,57 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
return -1;
}
// Crear vertex buffer para sprites
id<MTLBuffer> spriteVertexBuffer = [device newBufferWithLength:1024 options:MTLResourceCPUCacheModeDefaultCache];
// Crear vertex buffer para sprites (para 5 sprites = 30 vértices)
id<MTLBuffer> spriteVertexBuffer = [device newBufferWithLength:4096 options:MTLResourceCPUCacheModeDefaultCache];
if (!spriteVertexBuffer) {
std::cout << "Failed to create sprite vertex buffer" << std::endl;
return -1;
}
std::cout << "Metal pipelines created successfully (background + triangle + sprites)" << std::endl;
// Crear offscreen render target para CRT post-processing
CGSize drawableSize = metalLayer.drawableSize;
MTLTextureDescriptor* offscreenTextureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
width:(NSUInteger)drawableSize.width
height:(NSUInteger)drawableSize.height
mipmapped:NO];
offscreenTextureDescriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
id<MTLTexture> offscreenTexture = [device newTextureWithDescriptor:offscreenTextureDescriptor];
if (!offscreenTexture) {
std::cout << "Failed to create offscreen render target" << std::endl;
return -1;
}
// Crear pipeline CRT post-processing
MTLRenderPipelineDescriptor* crtPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
crtPipelineDescriptor.vertexFunction = crtVertexFunction;
crtPipelineDescriptor.fragmentFunction = crtFragmentFunction;
crtPipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat;
id<MTLRenderPipelineState> crtPipelineState = [device newRenderPipelineStateWithDescriptor:crtPipelineDescriptor error:&error];
if (!crtPipelineState || error) {
if (error) {
std::cout << "Failed to create CRT render pipeline: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
// Crear sampler para CRT (linear para suavizar la escalada)
MTLSamplerDescriptor* crtSamplerDescriptor = [[MTLSamplerDescriptor alloc] init];
crtSamplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear;
crtSamplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear;
crtSamplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge;
crtSamplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge;
id<MTLSamplerState> crtSampler = [device newSamplerStateWithDescriptor:crtSamplerDescriptor];
if (!crtSampler) {
std::cout << "Failed to create CRT sampler" << std::endl;
return -1;
}
std::cout << "Metal pipelines created successfully (background + triangle + sprites + CRT)" << std::endl;
std::cout << "Sprite texture loaded: " << [bitmap pixelsWide] << "x" << [bitmap pixelsHigh] << std::endl;
std::cout << "Offscreen render target created: " << (int)drawableSize.width << "x" << (int)drawableSize.height << std::endl;
// Main loop
bool quit = false;
@@ -444,12 +687,12 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
continue;
}
// Crear render pass descriptor
MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments[0].texture = drawable.texture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
// PASO 1: Renderizar escena a offscreen texture
MTLRenderPassDescriptor* offscreenRenderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
offscreenRenderPassDescriptor.colorAttachments[0].texture = offscreenTexture;
offscreenRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
offscreenRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
offscreenRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
// Crear command buffer
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
@@ -457,58 +700,104 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
continue;
}
// Crear render command encoder
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
if (!renderEncoder) {
// Crear render command encoder para offscreen
id<MTLRenderCommandEncoder> offscreenEncoder = [commandBuffer renderCommandEncoderWithDescriptor:offscreenRenderPassDescriptor];
if (!offscreenEncoder) {
continue;
}
// 1. Dibujar fondo degradado primero
[renderEncoder setRenderPipelineState:backgroundPipelineState];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
[offscreenEncoder setRenderPipelineState:backgroundPipelineState];
[offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
// 2. Dibujar triángulo encima
[renderEncoder setRenderPipelineState:trianglePipelineState];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
[offscreenEncoder setRenderPipelineState:trianglePipelineState];
[offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
// 3. Dibujar sprite con alpha blending
[renderEncoder setRenderPipelineState:spritePipelineState];
// 3. Dibujar 5 sprites con diferentes colores
[offscreenEncoder setRenderPipelineState:spritePipelineState];
// Configurar textura y sampler
[renderEncoder setFragmentTexture:spriteTexture atIndex:0];
[renderEncoder setFragmentSamplerState:textureSampler atIndex:0];
// Configurar textura y sampler para sprites
[offscreenEncoder setFragmentTexture:spriteTexture atIndex:0];
[offscreenEncoder setFragmentSamplerState:textureSampler atIndex:0];
// Crear sprite centrado con un tamaño pequeño
SpriteVertex spriteVertices[6]; // 2 triángulos para formar un quad
// Crear 5 sprites en diferentes posiciones con diferentes colores
SpriteVertex spriteVertices[30]; // 5 sprites × 6 vértices cada uno
// Sprite centrado de 30x30 pixels en pantalla de 960x720 (mantiene proporción de 10px en 320x240)
// Configuración común
float spriteSize = 30.0f;
float windowWidth = 960.0f;
float windowHeight = 720.0f;
float halfWidth = (spriteSize / windowWidth);
float halfHeight = (spriteSize / windowHeight);
// Convertir a coordenadas NDC
float halfWidth = (spriteSize / windowWidth); // 0.1 en NDC
float halfHeight = (spriteSize / windowHeight); // 0.133 en NDC
// Posiciones de los 5 sprites (evitando el centro donde está el triángulo)
float positions[5][2] = {
{-0.6f, 0.6f}, // Esquina superior izquierda
{ 0.6f, 0.6f}, // Esquina superior derecha
{-0.6f, -0.6f}, // Esquina inferior izquierda
{ 0.6f, -0.6f}, // Esquina inferior derecha
{ 0.0f, -0.8f} // Centro inferior
};
// Primer triángulo (bottom-left, bottom-right, top-left)
spriteVertices[0] = {{-halfWidth, -halfHeight}, {0.0f, 1.0f}}; // bottom-left
spriteVertices[1] = {{ halfWidth, -halfHeight}, {1.0f, 1.0f}}; // bottom-right
spriteVertices[2] = {{-halfWidth, halfHeight}, {0.0f, 0.0f}}; // top-left
// Colores para cada sprite (RGBA)
float colors[5][4] = {
{1.0f, 0.0f, 0.0f, 1.0f}, // Rojo
{0.0f, 1.0f, 0.0f, 1.0f}, // Verde
{0.0f, 0.0f, 1.0f, 1.0f}, // Azul
{1.0f, 1.0f, 0.0f, 1.0f}, // Amarillo
{1.0f, 0.0f, 1.0f, 1.0f} // Magenta
};
// Segundo triángulo (bottom-right, top-right, top-left)
spriteVertices[3] = {{ halfWidth, -halfHeight}, {1.0f, 1.0f}}; // bottom-right
spriteVertices[4] = {{ halfWidth, halfHeight}, {1.0f, 0.0f}}; // top-right
spriteVertices[5] = {{-halfWidth, halfHeight}, {0.0f, 0.0f}}; // top-left
// Generar vértices para los 5 sprites
for (int i = 0; i < 5; i++) {
float centerX = positions[i][0];
float centerY = positions[i][1];
// Índice base para este sprite
int baseIndex = i * 6;
// Primer triángulo (bottom-left, bottom-right, top-left)
spriteVertices[baseIndex + 0] = {{centerX - halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {0.0f, 1.0f}};
spriteVertices[baseIndex + 1] = {{centerX + halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 1.0f}};
spriteVertices[baseIndex + 2] = {{centerX - halfWidth, centerY + halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {0.0f, 0.0f}};
// Segundo triángulo (bottom-right, top-right, top-left)
spriteVertices[baseIndex + 3] = {{centerX + halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 1.0f}};
spriteVertices[baseIndex + 4] = {{centerX + halfWidth, centerY + halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 0.0f}};
spriteVertices[baseIndex + 5] = {{centerX - halfWidth, centerY + halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {0.0f, 0.0f}};
}
// Copiar vértices al buffer
void* spriteData = [spriteVertexBuffer contents];
memcpy(spriteData, spriteVertices, sizeof(spriteVertices));
// Configurar vertex buffer y dibujar
[renderEncoder setVertexBuffer:spriteVertexBuffer offset:0 atIndex:0];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
// Configurar vertex buffer y dibujar todos los sprites en offscreen
[offscreenEncoder setVertexBuffer:spriteVertexBuffer offset:0 atIndex:0];
[offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:30];
[renderEncoder endEncoding];
[offscreenEncoder endEncoding];
// PASO 2: Aplicar CRT post-processing a la pantalla final
MTLRenderPassDescriptor* finalRenderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
finalRenderPassDescriptor.colorAttachments[0].texture = drawable.texture;
finalRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
finalRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
finalRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
// Crear encoder para CRT post-processing
id<MTLRenderCommandEncoder> crtEncoder = [commandBuffer renderCommandEncoderWithDescriptor:finalRenderPassDescriptor];
if (!crtEncoder) {
continue;
}
// Aplicar CRT shader al resultado offscreen
[crtEncoder setRenderPipelineState:crtPipelineState];
[crtEncoder setFragmentTexture:offscreenTexture atIndex:0];
[crtEncoder setFragmentSamplerState:crtSampler atIndex:0];
[crtEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
[crtEncoder endEncoding];
// Presentar drawable
[commandBuffer presentDrawable:drawable];