Files
vibe5-metal/source/main.cpp
T
JailDesigner 909635d76d 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>
2025-09-28 18:30:01 +02:00

819 lines
31 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <iostream>
#include <SDL3/SDL.h>
// Headers Metal usando Objective-C++
#ifdef __APPLE__
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#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<MTLDevice> 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<MTLCommandQueue> 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 <metal_stdlib>
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 <metal_stdlib>
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 <metal_stdlib>
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 <metal_stdlib>
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 <metal_stdlib>
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 <metal_stdlib>
using namespace metal;
struct SpriteVertexOut {
float4 position [[position]];
float4 color;
float2 texCoord;
};
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 * in.color; // Multiplicar textura por color de vértice para tinting
}
)";
// 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) {
std::cout << "Failed to compile background vertex shader: " << [[error localizedDescription] UTF8String] << std::endl;
}
return -1;
}
id<MTLLibrary> 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<MTLFunction> backgroundVertexFunction = [backgroundVertexLibrary newFunctionWithName:@"background_vertex_main"];
id<MTLFunction> backgroundFragmentFunction = [backgroundFragmentLibrary newFunctionWithName:@"background_fragment_main"];
// Compilar shaders de triángulo
id<MTLLibrary> 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<MTLLibrary> 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<MTLFunction> triangleVertexFunction = [triangleVertexLibrary newFunctionWithName:@"triangle_vertex_main"];
id<MTLFunction> triangleFragmentFunction = [triangleFragmentLibrary newFunctionWithName:@"triangle_fragment_main"];
// Compilar shaders de sprites
id<MTLLibrary> 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<MTLLibrary> 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<MTLFunction> spriteVertexFunction = [spriteVertexLibrary newFunctionWithName:@"sprite_vertex_main"];
id<MTLFunction> 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<MTLRenderPipelineState> 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<MTLRenderPipelineState> 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<MTLRenderPipelineState> 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<MTLTexture> 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<MTLSamplerState> 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<MTLBuffer> 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<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;
SDL_Event e;
while (!quit) {
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) {
quit = true;
}
}
// Obtener drawable del Metal layer
id<CAMetalDrawable> 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<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
if (!commandBuffer) {
continue;
}
// Crear render command encoder para offscreen
id<MTLRenderCommandEncoder> 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<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];
[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
}