#include #include // Headers Metal usando Objective-C++ #ifdef __APPLE__ #import #import #import #import #endif /* * METAL PERFORMANCE HUD - Debug FPS y métricas en tiempo real * * Para mostrar FPS y estadísticas de rendimiento, ejecutar con: * * MTL_HUD_ENABLED=1 ./vibe5_metal * * El HUD aparece en la esquina superior derecha y muestra: * - FPS (Frames Per Second) * - CPU y GPU usage * - Memory usage * - Device info y resolución * * Atajos de teclado disponibles cuando el HUD está activo: * - Command+F9: Enable/Disable HUD * - Command+F8: Enable/Disable Logging * - Command+F11: Reset Metrics * - Command+F12: Show Configuration Panel * * Para activar globalmente para todas las apps Metal: * launchctl setenv MTL_HUD_ENABLED 1 * * Para desactivar: * launchctl setenv MTL_HUD_ENABLED 0 */ // 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 }; int main(int argc, char* argv[]) { #ifdef __APPLE__ // Configurar SDL para usar Metal SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal"); // Inicializar SDL if (!SDL_Init(SDL_INIT_VIDEO)) { std::cout << "SDL_Init failed: " << SDL_GetError() << std::endl; return -1; } // Crear ventana SDL_Window* window = SDL_CreateWindow("vibe5_metal - Triangle + Gradient + Sprites", 960, 720, SDL_WINDOW_HIGH_PIXEL_DENSITY); if (!window) { std::cout << "SDL_CreateWindow failed: " << SDL_GetError() << std::endl; SDL_Quit(); return -1; } // Crear renderer SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr); if (!renderer) { std::cout << "SDL_CreateRenderer failed: " << SDL_GetError() << std::endl; SDL_DestroyWindow(window); SDL_Quit(); return -1; } std::cout << "SDL Window and Renderer created successfully" << std::endl; // Obtener Metal layer CAMetalLayer* metalLayer = (__bridge CAMetalLayer*)SDL_GetRenderMetalLayer(renderer); if (!metalLayer) { std::cout << "Failed to get Metal layer - SDL may not be using Metal driver" << std::endl; SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return -1; } // Obtener device Metal del layer id device = metalLayer.device; if (!device) { std::cout << "Failed to get Metal device from layer" << std::endl; SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return -1; } std::cout << "Got Metal device: " << [[device name] UTF8String] << std::endl; // Crear command queue id commandQueue = [device newCommandQueue]; if (!commandQueue) { std::cout << "Failed to create Metal command queue" << std::endl; SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); return -1; } // Shaders para fondo degradado NSString* backgroundVertexShaderSource = @R"( #include using namespace metal; struct VertexOut { float4 position [[position]]; float4 color; }; vertex VertexOut background_vertex_main(uint vertexID [[vertex_id]]) { VertexOut out; // Quad de pantalla completa en coordenadas NDC 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 }; // Gradiente de púrpura oscuro arriba a azul cyan abajo float4 colors[6] = { float4(0.2, 0.6, 0.8, 1.0), // Bottom left - cyan claro float4(0.2, 0.6, 0.8, 1.0), // Bottom right - cyan claro float4(0.3, 0.1, 0.5, 1.0), // Top left - púrpura oscuro float4(0.2, 0.6, 0.8, 1.0), // Bottom right - cyan claro float4(0.3, 0.1, 0.5, 1.0), // Top right - púrpura oscuro float4(0.3, 0.1, 0.5, 1.0) // Top left - púrpura oscuro }; out.position = float4(positions[vertexID], 0.0, 1.0); out.color = colors[vertexID]; return out; } )"; NSString* backgroundFragmentShaderSource = @R"( #include using namespace metal; struct VertexOut { float4 position [[position]]; float4 color; }; fragment float4 background_fragment_main(VertexOut in [[stage_in]]) { return in.color; } )"; // Shaders para triángulo NSString* triangleVertexShaderSource = @R"( #include using namespace metal; struct VertexOut { float4 position [[position]]; float4 color; }; vertex VertexOut triangle_vertex_main(uint vertexID [[vertex_id]]) { VertexOut out; // Triángulo simple en coordenadas normalized device coordinates float2 positions[3] = { float2( 0.0, 0.5), // Top float2(-0.5, -0.5), // Bottom left float2( 0.5, -0.5) // Bottom right }; float4 colors[3] = { float4(1, 0, 0, 1), // Red float4(0, 1, 0, 1), // Green float4(0, 0, 1, 1) // Blue }; out.position = float4(positions[vertexID], 0.0, 1.0); out.color = colors[vertexID]; return out; } )"; NSString* triangleFragmentShaderSource = @R"( #include using namespace metal; struct VertexOut { float4 position [[position]]; float4 color; }; fragment float4 triangle_fragment_main(VertexOut in [[stage_in]]) { return in.color; } )"; // Shaders para sprites con textura y color NSString* spriteVertexShaderSource = @R"( #include using namespace metal; struct SpriteVertexIn { float2 position [[attribute(0)]]; 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; } )"; NSString* spriteFragmentShaderSource = @R"( #include using namespace metal; struct SpriteVertexOut { float4 position [[position]]; float4 color; float2 texCoord; }; 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 * in.color; // Multiplicar textura por color de vértice para tinting } )"; // 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) { std::cout << "Failed to compile background vertex shader: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } id backgroundFragmentLibrary = [device newLibraryWithSource:backgroundFragmentShaderSource options:nil error:&error]; if (!backgroundFragmentLibrary || error) { if (error) { std::cout << "Failed to compile background fragment shader: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } id backgroundVertexFunction = [backgroundVertexLibrary newFunctionWithName:@"background_vertex_main"]; id backgroundFragmentFunction = [backgroundFragmentLibrary newFunctionWithName:@"background_fragment_main"]; // Compilar shaders de triángulo id triangleVertexLibrary = [device newLibraryWithSource:triangleVertexShaderSource options:nil error:&error]; if (!triangleVertexLibrary || error) { if (error) { std::cout << "Failed to compile triangle vertex shader: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } id triangleFragmentLibrary = [device newLibraryWithSource:triangleFragmentShaderSource options:nil error:&error]; if (!triangleFragmentLibrary || error) { if (error) { std::cout << "Failed to compile triangle fragment shader: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } id triangleVertexFunction = [triangleVertexLibrary newFunctionWithName:@"triangle_vertex_main"]; id triangleFragmentFunction = [triangleFragmentLibrary newFunctionWithName:@"triangle_fragment_main"]; // Compilar shaders de sprites id spriteVertexLibrary = [device newLibraryWithSource:spriteVertexShaderSource options:nil error:&error]; if (!spriteVertexLibrary || error) { if (error) { std::cout << "Failed to compile sprite vertex shader: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } id spriteFragmentLibrary = [device newLibraryWithSource:spriteFragmentShaderSource options:nil error:&error]; if (!spriteFragmentLibrary || error) { if (error) { std::cout << "Failed to compile sprite fragment shader: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } id spriteVertexFunction = [spriteVertexLibrary newFunctionWithName:@"sprite_vertex_main"]; id spriteFragmentFunction = [spriteFragmentLibrary newFunctionWithName:@"sprite_fragment_main"]; // Crear pipeline de fondo MTLRenderPipelineDescriptor* backgroundPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; backgroundPipelineDescriptor.vertexFunction = backgroundVertexFunction; backgroundPipelineDescriptor.fragmentFunction = backgroundFragmentFunction; backgroundPipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat; id backgroundPipelineState = [device newRenderPipelineStateWithDescriptor:backgroundPipelineDescriptor error:&error]; if (!backgroundPipelineState || error) { if (error) { std::cout << "Failed to create background render pipeline: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } // Crear pipeline de triángulo MTLRenderPipelineDescriptor* trianglePipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; trianglePipelineDescriptor.vertexFunction = triangleVertexFunction; trianglePipelineDescriptor.fragmentFunction = triangleFragmentFunction; trianglePipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat; id trianglePipelineState = [device newRenderPipelineStateWithDescriptor:trianglePipelineDescriptor error:&error]; if (!trianglePipelineState || error) { if (error) { std::cout << "Failed to create triangle render pipeline: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } // Crear pipeline de sprites con alpha blending MTLRenderPipelineDescriptor* spritePipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; spritePipelineDescriptor.vertexFunction = spriteVertexFunction; spritePipelineDescriptor.fragmentFunction = spriteFragmentFunction; spritePipelineDescriptor.colorAttachments[0].pixelFormat = metalLayer.pixelFormat; // Configurar alpha blending spritePipelineDescriptor.colorAttachments[0].blendingEnabled = YES; spritePipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; spritePipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; spritePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; spritePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha; spritePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; spritePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; // 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 = 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; spritePipelineDescriptor.vertexDescriptor = spriteVertexDescriptor; id spritePipelineState = [device newRenderPipelineStateWithDescriptor:spritePipelineDescriptor error:&error]; if (!spritePipelineState || error) { if (error) { std::cout << "Failed to create sprite render pipeline: " << [[error localizedDescription] UTF8String] << std::endl; } return -1; } // Cargar textura de sprite NSString* texturePath = @"data/ball.png"; NSImage* image = [[NSImage alloc] initWithContentsOfFile:texturePath]; if (!image) { std::cout << "Failed to load texture image: data/ball.png" << std::endl; return -1; } // Obtener representación como bitmap NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc] initWithData:[image TIFFRepresentation]]; if (!bitmap) { std::cout << "Failed to create bitmap from image" << std::endl; return -1; } // Crear descriptor de textura Metal MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:[bitmap pixelsWide] height:[bitmap pixelsHigh] mipmapped:NO]; // Crear textura Metal id spriteTexture = [device newTextureWithDescriptor:textureDescriptor]; if (!spriteTexture) { std::cout << "Failed to create Metal texture" << std::endl; return -1; } // Copiar datos de imagen a textura Metal MTLRegion region = MTLRegionMake2D(0, 0, [bitmap pixelsWide], [bitmap pixelsHigh]); [spriteTexture replaceRegion:region mipmapLevel:0 withBytes:[bitmap bitmapData] bytesPerRow:[bitmap bytesPerRow]]; // Crear sampler con filtro NEAREST para evitar blur MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; samplerDescriptor.minFilter = MTLSamplerMinMagFilterNearest; samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest; samplerDescriptor.sAddressMode = MTLSamplerAddressModeClampToEdge; samplerDescriptor.tAddressMode = MTLSamplerAddressModeClampToEdge; id textureSampler = [device newSamplerStateWithDescriptor:samplerDescriptor]; if (!textureSampler) { std::cout << "Failed to create Metal sampler" << std::endl; return -1; } // 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; } // 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; SDL_Event e; while (!quit) { while (SDL_PollEvent(&e)) { if (e.type == SDL_EVENT_QUIT) { quit = true; } } // Obtener drawable del Metal layer id drawable = [metalLayer nextDrawable]; if (!drawable) { continue; } // 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]; if (!commandBuffer) { continue; } // Crear render command encoder para offscreen id offscreenEncoder = [commandBuffer renderCommandEncoderWithDescriptor:offscreenRenderPassDescriptor]; if (!offscreenEncoder) { continue; } // 1. Dibujar fondo degradado primero [offscreenEncoder setRenderPipelineState:backgroundPipelineState]; [offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6]; // 2. Dibujar triángulo encima [offscreenEncoder setRenderPipelineState:trianglePipelineState]; [offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; // 3. Dibujar 5 sprites con diferentes colores [offscreenEncoder setRenderPipelineState:spritePipelineState]; // Configurar textura y sampler para sprites [offscreenEncoder setFragmentTexture:spriteTexture atIndex:0]; [offscreenEncoder setFragmentSamplerState:textureSampler atIndex:0]; // Crear 5 sprites en diferentes posiciones con diferentes colores SpriteVertex spriteVertices[30]; // 5 sprites × 6 vértices cada uno // Configuración común float spriteSize = 30.0f; float windowWidth = 960.0f; float windowHeight = 720.0f; float halfWidth = (spriteSize / windowWidth); float halfHeight = (spriteSize / windowHeight); // 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 }; // 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 }; // 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 todos los sprites en offscreen [offscreenEncoder setVertexBuffer:spriteVertexBuffer offset:0 atIndex:0]; [offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:30]; [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]; [commandBuffer commit]; } // Cleanup SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); std::cout << "vibe5_metal finished successfully" << std::endl; return 0; #else std::cout << "This example requires macOS and Metal support" << std::endl; return -1; #endif }