diff --git a/data/crtpi_240.glsl b/data/crtpi_240.glsl new file mode 100644 index 0000000..068a6f1 --- /dev/null +++ b/data/crtpi_240.glsl @@ -0,0 +1,234 @@ +/* + crt-pi - A Raspberry Pi friendly CRT shader. + + Copyright (C) 2015-2016 davej + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + +Notes: + +This shader is designed to work well on Raspberry Pi GPUs (i.e. 1080P @ 60Hz on a game with a 4:3 aspect ratio). It pushes the Pi's GPU hard and enabling some features will slow it down so that it is no longer able to match 1080P @ 60Hz. You will need to overclock your Pi to the fastest setting in raspi-config to get the best results from this shader: 'Pi2' for Pi2 and 'Turbo' for original Pi and Pi Zero. Note: Pi2s are slower at running the shader than other Pis, this seems to be down to Pi2s lower maximum memory speed. Pi2s don't quite manage 1080P @ 60Hz - they drop about 1 in 1000 frames. You probably won't notice this, but if you do, try enabling FAKE_GAMMA. + +SCANLINES enables scanlines. You'll almost certainly want to use it with MULTISAMPLE to reduce moire effects. SCANLINE_WEIGHT defines how wide scanlines are (it is an inverse value so a higher number = thinner lines). SCANLINE_GAP_BRIGHTNESS defines how dark the gaps between the scan lines are. Darker gaps between scan lines make moire effects more likely. + +GAMMA enables gamma correction using the values in INPUT_GAMMA and OUTPUT_GAMMA. FAKE_GAMMA causes it to ignore the values in INPUT_GAMMA and OUTPUT_GAMMA and approximate gamma correction in a way which is faster than true gamma whilst still looking better than having none. You must have GAMMA defined to enable FAKE_GAMMA. + +CURVATURE distorts the screen by CURVATURE_X and CURVATURE_Y. Curvature slows things down a lot. + +By default the shader uses linear blending horizontally. If you find this too blury, enable SHARPER. + +BLOOM_FACTOR controls the increase in width for bright scanlines. + +MASK_TYPE defines what, if any, shadow mask to use. MASK_BRIGHTNESS defines how much the mask type darkens the screen. + +*/ + +#pragma parameter CURVATURE_X "Screen curvature - horizontal" 0.10 0.0 1.0 0.01 +#pragma parameter CURVATURE_Y "Screen curvature - vertical" 0.15 0.0 1.0 0.01 +#pragma parameter MASK_BRIGHTNESS "Mask brightness" 0.70 0.0 1.0 0.01 +#pragma parameter SCANLINE_WEIGHT "Scanline weight" 6.0 0.0 15.0 0.1 +#pragma parameter SCANLINE_GAP_BRIGHTNESS "Scanline gap brightness" 0.12 0.0 1.0 0.01 +#pragma parameter BLOOM_FACTOR "Bloom factor" 1.5 0.0 5.0 0.01 +#pragma parameter INPUT_GAMMA "Input gamma" 2.4 0.0 5.0 0.01 +#pragma parameter OUTPUT_GAMMA "Output gamma" 2.2 0.0 5.0 0.01 + +// Haven't put these as parameters as it would slow the code down. +#define SCANLINES +#define MULTISAMPLE +#define GAMMA +//#define FAKE_GAMMA +//#define CURVATURE +//#define SHARPER +// MASK_TYPE: 0 = none, 1 = green/magenta, 2 = trinitron(ish) +#define MASK_TYPE 2 + + +#ifdef GL_ES +#define COMPAT_PRECISION mediump +precision mediump float; +#else +#define COMPAT_PRECISION +#endif + +#ifdef PARAMETER_UNIFORM +uniform COMPAT_PRECISION float CURVATURE_X; +uniform COMPAT_PRECISION float CURVATURE_Y; +uniform COMPAT_PRECISION float MASK_BRIGHTNESS; +uniform COMPAT_PRECISION float SCANLINE_WEIGHT; +uniform COMPAT_PRECISION float SCANLINE_GAP_BRIGHTNESS; +uniform COMPAT_PRECISION float BLOOM_FACTOR; +uniform COMPAT_PRECISION float INPUT_GAMMA; +uniform COMPAT_PRECISION float OUTPUT_GAMMA; +#else +#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 +#endif + +/* COMPATIBILITY + - GLSL compilers +*/ + +//uniform vec2 TextureSize; +#if defined(CURVATURE) +varying vec2 screenScale; +#endif +varying vec2 TEX0; +varying float filterWidth; + +#if defined(VERTEX) +//uniform mat4 MVPMatrix; +//attribute vec4 VertexCoord; +//attribute vec2 TexCoord; +//uniform vec2 InputSize; +//uniform vec2 OutputSize; + +void main() +{ +#if defined(CURVATURE) + screenScale = vec2(1.0, 1.0); //TextureSize / InputSize; +#endif + filterWidth = (768.0 / 240.0) / 3.0; + TEX0 = vec2(gl_MultiTexCoord0.x, 1.0-gl_MultiTexCoord0.y)*1.0001; + gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; +} +#elif defined(FRAGMENT) + +uniform sampler2D Texture; + +#if defined(CURVATURE) +vec2 Distort(vec2 coord) +{ + vec2 CURVATURE_DISTORTION = vec2(CURVATURE_X, CURVATURE_Y); + // Barrel distortion shrinks the display area a bit, this will allow us to counteract that. + vec2 barrelScale = 1.0 - (0.23 * CURVATURE_DISTORTION); + coord *= screenScale; + coord -= vec2(0.5); + float rsq = coord.x * coord.x + coord.y * coord.y; + coord += coord * (CURVATURE_DISTORTION * rsq); + coord *= barrelScale; + if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) + coord = vec2(-1.0); // If out of bounds, return an invalid value. + else + { + coord += vec2(0.5); + coord /= screenScale; + } + + return coord; +} +#endif + +float CalcScanLineWeight(float dist) +{ + return max(1.0-dist*dist*SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS); +} + +float CalcScanLine(float dy) +{ + float scanLineWeight = CalcScanLineWeight(dy); +#if defined(MULTISAMPLE) + scanLineWeight += CalcScanLineWeight(dy-filterWidth); + scanLineWeight += CalcScanLineWeight(dy+filterWidth); + scanLineWeight *= 0.3333333; +#endif + return scanLineWeight; +} + +void main() +{ + vec2 TextureSize = vec2(320.0, 240.0); +#if defined(CURVATURE) + vec2 texcoord = Distort(TEX0); + if (texcoord.x < 0.0) + gl_FragColor = vec4(0.0); + else +#else + vec2 texcoord = TEX0; +#endif + { + vec2 texcoordInPixels = texcoord * TextureSize; +#if defined(SHARPER) + vec2 tempCoord = floor(texcoordInPixels) + 0.5; + vec2 coord = tempCoord / TextureSize; + vec2 deltas = texcoordInPixels - tempCoord; + float scanLineWeight = CalcScanLine(deltas.y); + vec2 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; + vec2 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); + float signY = sign(dy); + dy = dy * dy; + dy = dy * dy; + dy *= 8.0; + dy /= TextureSize.y; + dy *= signY; + vec2 tc = vec2(texcoord.x, yCoord + dy); +#endif + + vec3 colour = texture2D(Texture, tc).rgb; + +#if defined(SCANLINES) +#if defined(GAMMA) +#if defined(FAKE_GAMMA) + colour = colour * colour; +#else + colour = pow(colour, vec3(INPUT_GAMMA)); +#endif +#endif + scanLineWeight *= BLOOM_FACTOR; + colour *= scanLineWeight; + +#if defined(GAMMA) +#if defined(FAKE_GAMMA) + colour = sqrt(colour); +#else + colour = pow(colour, vec3(1.0/OUTPUT_GAMMA)); +#endif +#endif +#endif +#if MASK_TYPE == 0 + gl_FragColor = vec4(colour, 1.0); +#else +#if MASK_TYPE == 1 + float whichMask = fract((gl_FragCoord.x*1.0001) * 0.5); + vec3 mask; + if (whichMask < 0.5) + mask = vec3(MASK_BRIGHTNESS, 1.0, MASK_BRIGHTNESS); + else + mask = vec3(1.0, MASK_BRIGHTNESS, 1.0); +#elif MASK_TYPE == 2 + float whichMask = fract((gl_FragCoord.x*1.0001) * 0.3333333); + vec3 mask = vec3(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 + + gl_FragColor = vec4(colour * mask, 1.0); +#endif + } +} +#endif diff --git a/source/main.cpp b/source/main.cpp index 9107ef7..6011a23 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -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 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 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 +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 +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 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 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 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 crtVertexFunction = [crtVertexLibrary newFunctionWithName:@"crt_vertex_main"]; + id crtFragmentFunction = [crtFragmentLibrary newFunctionWithName:@"crt_fragment_main"]; + + // Compilar shaders de fondo id 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 spriteVertexBuffer = [device newBufferWithLength:1024 options:MTLResourceCPUCacheModeDefaultCache]; + // Crear vertex buffer para sprites (para 5 sprites = 30 vértices) + id 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 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 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 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 commandBuffer = [commandQueue commandBuffer]; @@ -457,58 +700,104 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]], continue; } - // Crear render command encoder - id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - if (!renderEncoder) { + // Crear render command encoder para offscreen + id 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 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];