Metal(2)-셰이더 코드 작성까지
이전 글 요약
https://codeisfuture.tistory.com/119
Metal(2)-셰이더 코드 작성까지
이전 글에서 메탈이란 GPU에 접근하여 빠른 그래픽 처리를 가능하게 해주는 저수준 API라는 것을 학습했다. 즉 Spiritekit, Animation 밑의 있는 것!! 이번엔 그래서 메탈이 어떻게 렌더링을 하는지 살펴
codeisfuture.tistory.com
이전 글에서 Metal이란 GPU에 접근하여 빠른 그래픽 처리를 가능하게 해주는 저수준 API라는 것을 학습했습니다.
즉 SpriteKit, Core Animation 밑에 있는 기반 기술이죠!
이번엔 Metal이 어떻게 렌더링을 하는지, 그리고 실제로 커스텀 필터를 어떻게 만드는지 자세히 살펴볼 계획입니다
렌더링 프로세스
Metal은 '초기화 단계'와 '렌더 패스 단계'로 나뉩니다.
이 구분이 중요한 이유는 성능 최적화 때문이에요!
초기화 단계 (앱 수명주기와 함께하는 객체들)
MTLDevice
let device = MTLCreateSystemDefaultDevice()
모든 Metal 객체 생성의 시작점입니다.
물리적 GPU를 추상화한 인터페이스라고 보면 되는데, 실제로는 디바이스별로 성능이 천차만별이에요.
디바이스별 최적화 전략
private func configureForDevice(_ device: MTLDevice) {
if device.supportsFamily(.apple8) { // A16 이상 (iPhone 14 Pro)
maxTextureSize = 8192
preferredThreadgroupSize = MTLSize(width: 32, height: 32, depth: 1)
supportsTileShading = true
} else if device.supportsFamily(.apple5) { // A12 이상
maxTextureSize = 4096
preferredThreadgroupSize = MTLSize(width: 16, height: 16, depth: 1)
supportsTileShading = true
} else {
maxTextureSize = 2048
preferredThreadgroupSize = MTLSize(width: 8, height: 8, depth: 1)
supportsTileShading = false
}
print("=== Metal 기능 지원 현황 ===")
print("최대 스레드 그룹 크기: \(device.maxThreadsPerThreadgroup)")
print("최대 버퍼 길이: \(device.maxBufferLength / 1024 / 1024)MB")
print("Tile shading 지원: \(supportsTileShading)")
}
실제로는 iPhone 8과 iPhone 15 Pro의 GPU 성능이 10배 이상 차이가 나더라고요! 😱
MTLCommandQueue
let commandQueue = device.makeCommandQueue()
GPU에 보낼 명령들을 순서대로 관리하는 대기열입니다
실제로 여러 프레임의 렌더링 명령이 동시에 큐에 들어갈 수 있으며, 이 큐를 통해 CPU와 GPU가 효율적으로 병렬 작업을 수행합니다.
CAMetalLayer
let metalLayer = CAMetalLayer()
metalLayer.device = device
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.framebufferOnly = true
metalLayer.frame = view.layer.frame
view.layer.addSublayer(metalLayer)
- pixelFormat: 텍스처의 픽셀 형식 지정
- framebufferOnly: 최적화를 위해 true로 설정 (읽기 전용으로 설정)
- frame: 화면에 표시될 영역 설정
GPU가 그린 이미지를 실제로 보여주는 역할을 담당합니다.
MTLLibrary 및 MTLFunction - 셰이더 관리
private func setupShaderLibrary() {
// 방법 1: 빌드 타임 컴파일 (권장 - 성능 최적)
defaultLibrary = device.makeDefaultLibrary()
#if DEBUG
// 방법 2: 런타임 컴파일 (개발/테스트용)
if let shaderSource = loadShaderSource() {
do {
let runtimeLibrary = try device.makeLibrary(source: shaderSource, options: nil)
print("런타임 셰이더 컴파일 성공 - 개발 모드에서만 사용!")
} catch {
print("런타임 컴파일 실패: \(error)")
}
}
#endif
}
let vertexFunction = library?.makeFunction(name: "vertexPassThrough")
let fragmentFunction = library?.makeFunction(name: "fragmentPassThrough")
MTLLibrary는 GPU에서 실행될 셰이더 코드의 컬렉션이고,
MTLFunction은 라이브러리에서 특정 셰이더 함수를 나타냅니다.
빌드 타임에 컴파일하는 게 훨씬 빠르니까 릴리즈에서는 런타임 컴파일을 피하는 게 좋아요!
MTLRenderPipelineState
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
let pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor)
MTLRenderPipelineState는 그래픽 렌더링 과정 전체를 정의합니다.
위에서 만든 MTLFunction들을 여기서 연결한다고 보면 되고, 렌더링 중에는 수정할 수 없으므로 모든 설정을 미리 완료해야 합니다.
MTLBuffer
let vertexData: [Float] = [
0, 1, // 상단 중앙 점 (x, y)
-1, -1, // 좌측 하단 점 (x, y)
1, -1 // 우측 하단 점 (x, y)
]
let vertexDataSize = vertexData.count * MemoryLayout<Float>.size
let vertexBuffer = device.makeBuffer(bytes: vertexData,
length: vertexDataSize,
options: [])
CPU와 GPU 간에 공유되는 메모리 영역으로, 셰이더에 전달할 데이터를 저장합니다.
렌더 패스 단계(매프레임마다 생성되는 객체들)
Drawable
let drawable = metalLayer.nextDrawable()
CAMetalDrawable은 화면에 표시할 준비가 된 텍스처를 제공합니다.
매 프레임마다 새로운 Drawable을 요청해야 하고, 한 프레임이 화면에 표시되는 동안 다음 프레임을 준비할 수 있게 합니다.
MTLRenderPassDescriptor
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = .init(red: 0, green: 0, blue: 0, alpha: 1)
렌더링 작업에 대한 세부사항 정의한다.
- texture: 그림을 그릴 텍스처 지정
- loadAction: 렌더링 시작 전 텍스처 처리 방법
- clearColor: 텍스처를 지울 때 사용할 색상
MTLCommandBuffer 및 MTLRenderCommandEncoder
let commandBuffer = commandQueue.makeCommandBuffer()
let commandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
commandEncoder?.setRenderPipelineState(pipelineState)
commandEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
commandEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertexData.count/2)
commandEncoder?.endEncoding()
GPU에 보낼 명령들을 모아두는 버퍼입니다. MTLRenderCommandEncoder는 명령을 GPU가 이해할 수 있는 형식으로 인코딩합니다.
- setRenderPipelineState: 사용할 파이프라인 설정
- setVertexBuffer: 사용할 버텍스 버퍼 설정 (index: 0은 셰이더에서 buffer(0)으로 접근)
- drawPrimitives: 실제 그리기 명령
drawPrimitives의 type 옵션들:
- .triangle: 3개 버텍스를 연결하여 삼각형 그리기
- .triangleStrip: 연속된 삼각형 그리기 (인접 삼각형이 변을 공유)
- .line: 선 그리기
- .point: 점 그리기
Drawable 등록 및 CommandBuffer 커밋
commandBuffer.present(drawable)
commandBuffer.commit()
마지막으로, 그려진 결과를 화면에 표시하도록 등록하고 명령을 커밋합니다.
- present: 그려진 결과를 화면에 표시할 준비
- commit: 모든 명령을 GPU에 전송하여 실행
커스텀 이미지 필터 구현하기
자, 여기까지가 어떻게 렌더링되는 과정이었습니다. 제가 원하는 건 사진에 필터를 입히는 거예요.
CIFilter 같은 완성형 API 말고 나만의 필터를 만들어보고 싶었거든요!
Metal 순서도
UIImage 입력
↓
1. 데이터 변환 (CPU → GPU)
UIImage → CGImage → MTLTexture
↓
2. 출력 텍스처 준비
빈 MTLTexture 생성 (결과 저장용)
↓
3. 파이프라인 설정
셰이더 함수 로드 → 컴파일 → 캐싱
↓
4. 명령 준비
CommandBuffer + CommandEncoder 생성
↓
5. 데이터 바인딩
텍스처 연결 + 파라미터 전달
↓
6. 스레드 배치
GPU 코어들에게 작업 분배
↓
7. GPU 실행
병렬로 필터 적용
↓
8. 완료 대기
GPU 작업 완료까지 대기
↓
9. 결과 변환 (GPU → CPU)
MTLTexture → UIImage
↓
완성된 필터 이미지 반환
더 자세한 코드 흐름으로 보면:
Swift에서 필터 호출
let filteredImage = applyFilter(originalImage, filterType: "neon_glow", intensity: 0.8)
데이터 변환 단계
// CPU 메모리 → GPU 메모리
guard let cgImage = image.cgImage else { return image }
let textureLoader = MTKTextureLoader(device: device)
guard let inTexture = try? textureLoader.newTexture(cgImage: cgImage, options: options)
왜 변환이 필요할까요?
GPU는 CPU와 완전히 분리된 메모리 공간을 사용합니다.
UIImage는 CPU 메모리에 있는 데이터라서 GPU가 직접 접근할 수 없어요.
그래서 MTKTextureLoader를 사용해 GPU 메모리로 데이터를 복사해야 합니다.
출력 공간 준비
let descriptor = MTLTextureDescriptor.texture2DDescriptor(
pixelFormat: inTexture.pixelFormat, // 입력과 동일한 형식으로 호환성 유지
width: inTexture.width,
height: inTexture.height,
mipmapped: false // 2D 이미지에는 불필요
)
descriptor.usage = [.shaderRead, .shaderWrite] // 읽기/쓰기 모두 가능하게 설정
guard let outTexture = device.makeTexture(descriptor: descriptor) else { return image }
필터 적용 결과를 저장할 GPU 메모리 공간 할당
Mipmap이 뭔가요?
Mipmap은 같은 이미지를 여러 해상도로 미리 만들어놓은 것입니다.
3D 게임에서 멀리 있는 텍스처를 효율적으로 렌더링할 때 사용하는데, 2D 이미지 필터링에는 불필요해서 false로 설정합니다.
셰이더 준비 (한 번만 실행)
// 파이프라인 캐싱 체크
if pipelineStateCache[functionName] == nil {
let function = defaultLibrary.makeFunction(name: functionName)
let pipelineState = device.makeComputePipelineState(function: function)
pipelineStateCache[functionName] = pipelineState
}
Metal 셰이더를 GPU가 실행할 수 있는 바이너리로 컴파일
파이프라인 생성이 왜 느릴까요?
Metal 셰이더를 GPU가 실행할 수 있는 바이너리 코드로 컴파일하는 과정이 포함되어 있어서 시간이 걸립니다.
한 번 만들어두고 재사용하면 성능이 크게 향상되어요!
명령 버퍼와 인코더 설정
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let encoder = commandBuffer.makeComputeCommandEncoder() else {
return image
}
// 파이프라인과 데이터 설정
encoder.setComputePipelineState(pipelineState)
encoder.setTexture(inTexture, index: 0) // [[texture(0)]]와 매칭
encoder.setTexture(outTexture, index: 1) // [[texture(1)]]와 매칭
var intensityValue = intensity
encoder.setBytes(&intensityValue, length: MemoryLayout<Float>.size, index: 0) // [[buffer(0)]]와 매칭
GPU에게 줄 명령서와 명령 작성 도구 준비하고 셰이더 함수에 필요한 재료들 전달
Index 값이 중요한 이유
Swift 코드의 index 값과 Metal 셰이더의 [[texture(0)]], [[buffer(0)]] 값이 정확히 일치해야 합니다. 안 그러면 데이터가 엉뚱한 곳으로 전달되어 예상치 못한 결과가 나와요!
스레드 배치 (작업 분할)
// GPU 성능에 따른 최적 스레드 그룹 크기 설정
let threadGroupSize: MTLSize
if device.supportsFamily(.apple7) { // A15 이상
threadGroupSize = MTLSize(width: 32, height: 32, depth: 1) // 1024 threads
} else if device.supportsFamily(.apple4) { // A11 이상
threadGroupSize = MTLSize(width: 16, height: 16, depth: 1) // 256 threads
} else {
threadGroupSize = MTLSize(width: 8, height: 8, depth: 1) // 64 threads
}
let threadGroupCount = MTLSize(
width: (inTexture.width + threadGroupSize.width - 1) / threadGroupSize.width,
height: (inTexture.height + threadGroupSize.height - 1) / threadGroupSize.height,
depth: 1
)
encoder.dispatchThreadgroups(threadGroupCount, threadsPerThreadgroup: threadGroupSize)
각 픽셀마다 GPU 코어 하나씩 할당해서 병렬 처리
스레드 그룹이 뭔가요?
GPU는 수백 개의 작은 코어가 동시에 작업합니다. 스레드 그룹은 이 코어들을 몇 개씩 묶어서 관리하는 단위예요. 각 픽셀마다 하나의 스레드가 할당되어 병렬로 필터를 적용합니다.
GPU에서 실제 실행
kernel void neon_glow(texture2d<float, access::read> inTexture [[texture(0)]],
texture2d<float, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant float &intensity [[buffer(0)]]) {
// 이 함수가 각 픽셀마다 동시에 실행됨!
uint2 correctedGid = uint2(gid.x, inTexture.get_height() - 1 - gid.y);
float4 color = inTexture.read(correctedGid);
// ... 네온 글로우 효과 적용 ...
outTexture.write(result, gid);
}
수백만 개 픽셀이 동시에 필터 처리됨
완료 대기
commandBuffer.commit() // 명령 전송
commandBuffer.waitUntilCompleted() // 완료까지 기다림
GPU 작업이 끝날 때까지 기다림
결과 가져오기
let ciImage = CIImage(mtlTexture: outTexture)
let context = CIContext()
let cgImageOut = context.createCGImage(ciImage, from: ciImage.extent)
return UIImage(cgImage: cgImageOut)
좌표계 이슈와 해결 전략
문제 상황: 이미지가 뒤집혀서 나오는 현상
// 문제가 되는 변환 과정
UIImage → CGImage → MTLTexture → 셰이더 처리 → MTLTexture → CIImage → CGImage → UIImage
각 단계별 좌표계:
- MTLTexture: 좌상단 (0,0), Y축 아래로 증가
- UIImage: 좌상단 (0,0), Y축 아래로 증가 (하지만 내부적으로 orientation 정보 사용)
- Core Graphics: 좌하단 (0,0), Y축 위로 증가
- UIImage -> CGImage: UIImage의 cgImage속성을 호출하면 CGImage로 반환되면서 픽셀데이터는 좌측 하단 원점으로 저장된다.
- CGImage->MTLTexture: 메탈 좌표계에 맞춰 데이터가 로드된다. 이때 상하반전이 일어난다!!!
- 셰이더 처리: 셰이더는 MTLTexture를 읽고 출력 텍스처에 기록. 입력 출력 모두 메탈 좌표계를 따른다.
- MTLTexture->CIImage->CGImage->UIImage : 최종 UIimage생성시 orientation을 명시하지 않으면 .Up이 디폴트.
해결 방법 비교
방법 1: CPU에서 Orientation 보정
return UIImage(cgImage: cgImageOut,scale: 1.0,orientation: .downMirrored)
장점: 구현이 간단
단점: 고해상도 이미지에서 CPU 부담 증가
방법 2: 셰이더에서 좌표 보정
uint2 correctedGid = uint2(gid.x, inTexture.get_height() - 1 - gid.y);
장점: GPU에서 처리하므로 성능 우수
단점: 셰이더 코드가 약간 복잡해짐
저는 2번 방법을 선택했습니다.
GPU는 이미 픽셀당 수십 개의 연산을 하고 있어서 좌표 보정 정도는 성능에 거의 영향을 주지 않거든요
네온 글로우 필터 구현하기
1. 소벨 엣지 검출 (Sobel Edge Detection) - 윤곽선 찾기
와! 이거 대학교에서 수업 들었던 건데 기억이... 새록새록! 😅
소벨 엣지 검출은 이미지의 각 픽셀에서 밝기의 기울기(gradient)를 계산하여 엣지를 검출하는 방법입니다.
float sobelEdgeWithBoundary(texture2d<float, access::read> inTexture, uint2 gid) {
// 3x3 소벨 커널 정의
float3x3 sobelX = float3x3(
-1.0, 0.0, 1.0, // X축 방향 기울기 검출
-2.0, 0.0, 2.0, // 가운데 행에 가중치 2배
-1.0, 0.0, 1.0
);
float3x3 sobelY = float3x3(
-1.0, -2.0, -1.0, // Y축 방향 기울기 검출
0.0, 0.0, 0.0, // 가운데 열은 0
1.0, 2.0, 1.0
);
float gx = 0.0, gy = 0.0;
// 3x3 영역의 모든 픽셀 검사
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
// 경계를 벗어나지 않도록 클램핑 (중요!)
uint2 sampleCoord = uint2(
clamp(int(gid.x) + x, 0, int(inTexture.get_width()) - 1),
clamp(int(gid.y) + y, 0, int(inTexture.get_height()) - 1)
);
float4 color = inTexture.read(sampleCoord);
// RGB를 휘도(luminance)로 변환 - Rec.709 표준
float luminance = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
// 소벨 커널 적용
gx += luminance * sobelX[y+1][x+1];
gy += luminance * sobelY[y+1][x+1];
}
}
// 기울기 벡터의 크기 계산 (피타고라스 정리)
return sqrt(gx * gx + gy * gy);
}
소벨 엣지 검출 작동 원리
1. 소벨 커널이란?
- SobelX: X축 방향의 밝기 변화를 감지하는 3×3 필터
- SobelY: Y축 방향의 밝기 변화를 감지하는 3×3 필터
2. 휘도(Luminance) 변환 RGB 색상을 그레이스케일로 변환하는 과정입니다. 인간의 눈은 색상별로 민감도가 다르기 때문에 가중치를 적용해요:
- 녹색: 70.52% (가장 민감)
- 빨간색: 21.26%
- 파란색: 7.22% (가장 둔감)
3. 기울기 계산 주변 픽셀들과의 밝기 차이를 계산해서 X축, Y축 방향의 변화량을 구합니다.
4. 엣지 강도 계산 피타고라스 정리로 X, Y 방향 기울기를 합쳐서 최종 엣지 강도를 계산합니다.
2.네온글로우MSL
kernel void neon_glow(texture2d<float, access::read> inTexture [[texture(0)]],
texture2d<float, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant float &intensity [[buffer(0)]]) {
// 1. 경계 검사
if (gid.x >= inTexture.get_width() || gid.y >= inTexture.get_height()) {
return;
}
// 2. 좌표계 보정
uint2 correctedGid = uint2(gid.x, inTexture.get_height() - 1 - gid.y);
float4 originalColor = inTexture.read(correctedGid);
// 3. 엣지 강도 계산
float edgeStrength = sobelEdgeWithBoundary(inTexture, correctedGid);
edgeStrength *= 3.0 * intensity; // 엣지 효과 극대화
// 4. 네온 베이스 색상 생성 (보라/분홍 계열)
float3 neonBase = originalColor.rgb + float3(0.3, 0.15, 0.45) * intensity;
// 5. 엣지 부분에 강한 네온 색상 추가
float3 neonColor = neonBase + edgeStrength * intensity * float3(0.7, 0.6, 0.8);
// 6. 원본과 네온 효과 블렌딩 (선형 보간)
float3 blendedColor = mix(originalColor.rgb, neonColor, intensity);
// 7. 채도(Saturation) 증가 처리
float luminance = dot(originalColor.rgb, float3(0.2126, 0.7152, 0.0722));
blendedColor = mix(float3(luminance), blendedColor, 1.0 + intensity * 0.5);
// 8. 최종 결과 출력 (값 범위 제한)
outTexture.write(float4(clamp(blendedColor, 0.0, 1.0), originalColor.a), gid);
}
엣지 강화
edgeStrength *= 3.0 * intensity;
- 소벨 엣지 검출로 찾은 윤곽선을 3배 강화
- intensity로 사용자가 조절 가능
- 네온사인의 강렬한 빛 효과 재현
네온 베이스 색상
float3 neonBase = originalColor.rgb + float3(0.3, 0.15, 0.45) * intensity;
왜 이 비율일까요?
- 0.3, 0.15, 0.45 = 30% 빨강 + 15% 녹색 + 45% 파랑
- 결과적으로 보라색 계열 생성
- 실제 네온사인에서 가장 인상적인 색상이 보라/분홍이거든요!
엣지 하이라이트
float3 neonColor = neonBase + edgeStrength * intensity * float3(0.7, 0.6, 0.8);
- 엣지 부분에만 더 강한 네온 색상 추가
- 윤곽선이 빛나는 효과 구현
- float3(0.7, 0.6, 0.8) = 더 밝은 보라/분홍 계열
선형 보간 (Linear Interpolation)
float3 blendedColor = mix(originalColor.rgb, neonColor, intensity);
Mix 함수 동작 원리:
mix(a, b, t) = a × (1-t) + b × t
intensity = 0.0 → original × 1.0 + neon × 0.0 = 100% 원본
intensity = 0.5 → original × 0.5 + neon × 0.5 = 50% 원본 + 50% 네온
intensity = 1.0 → original × 0.0 + neon × 1.0 = 100% 네온
채도 증가
blendedColor = mix(float3(luminance), blendedColor, 1.0 + intensity * 0.5);
채도(Saturation)란? 색상이 얼마나 순수한지를 나타내는 값입니다.
- 높은 채도: 선명하고 생생한 색상
- 낮은 채도: 흐릿하고 회색빛 색상
채도 증가 과정:
- 그레이스케일 성분 제거: 색상에서 회색 부분을 빼기
- 색상 성분 증폭: 남은 순수한 색상을 강화
- 밝기 유지: 원래 휘도는 그대로 유지
.
CIKernel
:Custom Core Image Filter를 만드는데 사용되는 GPU기반 이미지 처리 루틴
kernerl Lanaguage routine의 리턴타입은 vec4(Core Image Kernel Language) or float4(Metal shading Language)
https://developer.apple.com/documentation/coreimage/cikernel
CIKernel | Apple Developer Documentation
A GPU-based image-processing routine used to create custom Core Image filters.
developer.apple.com
요건 공식문서 . 오버뷰를 보면
사용자 지정 필터가 색상과 지오메트리 정보를 모두 사용하지만 동시에 처리할 필요가 없는 경우 이미지 처리 코드를 분리하여 성능을 향상시킬 수 있습니다. 색상 처리 단계에는 CIColorKernel 객체를 사용하고 지오메트리 처리 단계에는 CIWarpKernel 객체를 사용합니다.
CIColorKernel: 픽셀 색상 값만 변경하는 필터에 사용. 이미지의 기하학적 구조는 변경하지 않는다.
- 색상 보정, 색조 조정, 블러효과
CIWrapKernel: 이미지의 기하학적 구조를 변형하는 필터. 픽셀 위치를 재배치
- 왜곡, 변형, 뒤틀림 효과
CIKernel: 일반적인 목적의 커널. 두개다 모두 수행 ㄱㄴ
일반적인 필터 커널 특징
- 반환 타입은 vec4(Core Image Kernel Language) 또는 float4(Metal Shading Language)입니다. 이는 출력 이미지의 픽셀 색상을 반환합니다.
- 0개 이상의 입력 이미지를 사용할 수 있으며, 각 입력 이미지는 sampler 타입의 매개변수로 표현됩니다.
커널 루틴의 작동방식1. 소스이미지 좌표 계산(destCoord와 samplerTransform함수사용)2. 소스이미지에서 샘플링(sample 함수 사용)3. 최종 픽셀 색상 계산(return 키워드 출력)
#include <CoreImage/CoreImage.h>
extern "C" {
namespace coreimage {
float4 do_nothing(sampler src) {
return src.sample(src.coord());
}
}
}
MSL로 작성했다. 이 코드는 입력 이미지를 변경 없이 그대로 통과시키는 간단한 필터 구현. src.sample(src.coord())는 현재 좌표의 픽셀 값을 그대로 가져온다.
그런데 저기서 extern "C"는 뭘까?
안에 정의된 함수 혹은 헤더파일에 관해서는 맹글링하지 말라는 뜻이다. 흠.. 그러면 맹글링이 뭔데? 댕글링포인터는 아는데 ㅎ
C++에서 맹글링은 컴파일러가 함수나 변수의 이름을 변환하는 과정입니다, 그러면 왜 이런게 있을까?
1. 오버로딩: C++은 같은 이름의 함수를 매개변수만 다르게 정의할수 있다.
2. 클래스범위: 다른클래스에 같은 이름의 함수가 있을 수 있다.
컴파일러는 이런함수들을 구별하기위해 내부적으로 함수이름을 맹글링(변환)한다. 하지만 extern "C"는 C++ 코드 안에서 이부분을 C방식으로 처리하라는 듯. 즉 저기 안에있는것은 맹글링 없이 그대로 그 이름대로 컴파일된다. 저 위의 예시코드에서는 함수이름은 do_nothing으로 유지되고 네임스페이스로 인해 외부에서는 coreimage::do_nothing으로 정의