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

234
data/crtpi_240.glsl Normal file
View File

@@ -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

View File

@@ -38,6 +38,7 @@
// Estructura para vértices de sprites // Estructura para vértices de sprites
struct SpriteVertex { struct SpriteVertex {
float position[2]; // x, y coordinates float position[2]; // x, y coordinates
float color[4]; // r, g, b, a color components
float texCoord[2]; // u, v texture coordinates 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"( NSString* spriteVertexShaderSource = @R"(
#include <metal_stdlib> #include <metal_stdlib>
using namespace metal; using namespace metal;
struct SpriteVertexIn { struct SpriteVertexIn {
float2 position [[attribute(0)]]; float2 position [[attribute(0)]];
float2 texCoord [[attribute(1)]]; float4 color [[attribute(1)]];
float2 texCoord [[attribute(2)]];
}; };
struct SpriteVertexOut { struct SpriteVertexOut {
float4 position [[position]]; float4 position [[position]];
float4 color;
float2 texCoord; float2 texCoord;
}; };
vertex SpriteVertexOut sprite_vertex_main(SpriteVertexIn in [[stage_in]]) { vertex SpriteVertexOut sprite_vertex_main(SpriteVertexIn in [[stage_in]]) {
SpriteVertexOut out; SpriteVertexOut out;
out.position = float4(in.position, 0.0, 1.0); out.position = float4(in.position, 0.0, 1.0);
out.color = in.color;
out.texCoord = in.texCoord; out.texCoord = in.texCoord;
return out; return out;
} }
@@ -233,6 +237,7 @@ using namespace metal;
struct SpriteVertexOut { struct SpriteVertexOut {
float4 position [[position]]; float4 position [[position]];
float4 color;
float2 texCoord; float2 texCoord;
}; };
@@ -240,12 +245,205 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
texture2d<float> spriteTexture [[texture(0)]], texture2d<float> spriteTexture [[texture(0)]],
sampler textureSampler [[sampler(0)]]) { sampler textureSampler [[sampler(0)]]) {
float4 textureColor = spriteTexture.sample(textureSampler, in.texCoord); 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; 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]; id<MTLLibrary> backgroundVertexLibrary = [device newLibraryWithSource:backgroundVertexShaderSource options:nil error:&error];
if (!backgroundVertexLibrary || error) { if (!backgroundVertexLibrary || error) {
if (error) { if (error) {
@@ -348,14 +546,17 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
spritePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; spritePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
spritePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; spritePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
// Configurar vertex descriptor para sprites // Configurar vertex descriptor para sprites (position + color + texCoord)
MTLVertexDescriptor* spriteVertexDescriptor = [[MTLVertexDescriptor alloc] init]; MTLVertexDescriptor* spriteVertexDescriptor = [[MTLVertexDescriptor alloc] init];
spriteVertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position spriteVertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position
spriteVertexDescriptor.attributes[0].offset = 0; spriteVertexDescriptor.attributes[0].offset = 0;
spriteVertexDescriptor.attributes[0].bufferIndex = 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].offset = 8;
spriteVertexDescriptor.attributes[1].bufferIndex = 0; 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].stride = sizeof(SpriteVertex);
spriteVertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; spriteVertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
@@ -417,15 +618,57 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
return -1; return -1;
} }
// Crear vertex buffer para sprites // Crear vertex buffer para sprites (para 5 sprites = 30 vértices)
id<MTLBuffer> spriteVertexBuffer = [device newBufferWithLength:1024 options:MTLResourceCPUCacheModeDefaultCache]; id<MTLBuffer> spriteVertexBuffer = [device newBufferWithLength:4096 options:MTLResourceCPUCacheModeDefaultCache];
if (!spriteVertexBuffer) { if (!spriteVertexBuffer) {
std::cout << "Failed to create sprite vertex buffer" << std::endl; std::cout << "Failed to create sprite vertex buffer" << std::endl;
return -1; 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 << "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 // Main loop
bool quit = false; bool quit = false;
@@ -444,12 +687,12 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
continue; continue;
} }
// Crear render pass descriptor // PASO 1: Renderizar escena a offscreen texture
MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; MTLRenderPassDescriptor* offscreenRenderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor];
renderPassDescriptor.colorAttachments[0].texture = drawable.texture; offscreenRenderPassDescriptor.colorAttachments[0].texture = offscreenTexture;
renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; offscreenRenderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear;
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0); offscreenRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.0, 0.0, 1.0);
renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; offscreenRenderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore;
// Crear command buffer // Crear command buffer
id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer]; id<MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer];
@@ -457,58 +700,104 @@ fragment float4 sprite_fragment_main(SpriteVertexOut in [[stage_in]],
continue; continue;
} }
// Crear render command encoder // Crear render command encoder para offscreen
id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; id<MTLRenderCommandEncoder> offscreenEncoder = [commandBuffer renderCommandEncoderWithDescriptor:offscreenRenderPassDescriptor];
if (!renderEncoder) { if (!offscreenEncoder) {
continue; continue;
} }
// 1. Dibujar fondo degradado primero // 1. Dibujar fondo degradado primero
[renderEncoder setRenderPipelineState:backgroundPipelineState]; [offscreenEncoder setRenderPipelineState:backgroundPipelineState];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6]; [offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6];
// 2. Dibujar triángulo encima // 2. Dibujar triángulo encima
[renderEncoder setRenderPipelineState:trianglePipelineState]; [offscreenEncoder setRenderPipelineState:trianglePipelineState];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; [offscreenEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3];
// 3. Dibujar sprite con alpha blending // 3. Dibujar 5 sprites con diferentes colores
[renderEncoder setRenderPipelineState:spritePipelineState]; [offscreenEncoder setRenderPipelineState:spritePipelineState];
// Configurar textura y sampler // Configurar textura y sampler para sprites
[renderEncoder setFragmentTexture:spriteTexture atIndex:0]; [offscreenEncoder setFragmentTexture:spriteTexture atIndex:0];
[renderEncoder setFragmentSamplerState:textureSampler atIndex:0]; [offscreenEncoder setFragmentSamplerState:textureSampler atIndex:0];
// Crear sprite centrado con un tamaño pequeño // Crear 5 sprites en diferentes posiciones con diferentes colores
SpriteVertex spriteVertices[6]; // 2 triángulos para formar un quad 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 spriteSize = 30.0f;
float windowWidth = 960.0f; float windowWidth = 960.0f;
float windowHeight = 720.0f; float windowHeight = 720.0f;
float halfWidth = (spriteSize / windowWidth);
float halfHeight = (spriteSize / windowHeight);
// Convertir a coordenadas NDC // Posiciones de los 5 sprites (evitando el centro donde está el triángulo)
float halfWidth = (spriteSize / windowWidth); // 0.1 en NDC float positions[5][2] = {
float halfHeight = (spriteSize / windowHeight); // 0.133 en NDC {-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) // Primer triángulo (bottom-left, bottom-right, top-left)
spriteVertices[0] = {{-halfWidth, -halfHeight}, {0.0f, 1.0f}}; // bottom-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[1] = {{ halfWidth, -halfHeight}, {1.0f, 1.0f}}; // bottom-right spriteVertices[baseIndex + 1] = {{centerX + halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 1.0f}};
spriteVertices[2] = {{-halfWidth, halfHeight}, {0.0f, 0.0f}}; // top-left 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) // Segundo triángulo (bottom-right, top-right, top-left)
spriteVertices[3] = {{ halfWidth, -halfHeight}, {1.0f, 1.0f}}; // bottom-right spriteVertices[baseIndex + 3] = {{centerX + halfWidth, centerY - halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 1.0f}};
spriteVertices[4] = {{ halfWidth, halfHeight}, {1.0f, 0.0f}}; // top-right spriteVertices[baseIndex + 4] = {{centerX + halfWidth, centerY + halfHeight}, {colors[i][0], colors[i][1], colors[i][2], colors[i][3]}, {1.0f, 0.0f}};
spriteVertices[5] = {{-halfWidth, halfHeight}, {0.0f, 0.0f}}; // top-left 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 // Copiar vértices al buffer
void* spriteData = [spriteVertexBuffer contents]; void* spriteData = [spriteVertexBuffer contents];
memcpy(spriteData, spriteVertices, sizeof(spriteVertices)); memcpy(spriteData, spriteVertices, sizeof(spriteVertices));
// Configurar vertex buffer y dibujar // Configurar vertex buffer y dibujar todos los sprites en offscreen
[renderEncoder setVertexBuffer:spriteVertexBuffer offset:0 atIndex:0]; [offscreenEncoder setVertexBuffer:spriteVertexBuffer offset:0 atIndex:0];
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:6]; [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 // Presentar drawable
[commandBuffer presentDrawable:drawable]; [commandBuffer presentDrawable:drawable];