Swift와 Metal로 떠나는 그래픽 프로그래밍 여행! 🚀✨
안녕, 그래픽 프로그래밍에 관심 있는 친구들! 오늘은 정말 흥미진진한 주제로 우리 함께 재미있는 여행을 떠나볼 거야. 바로 Swift와 Metal을 이용한 그래픽 프로그래밍이라는 멋진 세계로 말이지! 😎
혹시 재능넷에서 프로그래밍 관련 재능을 찾아본 적 있어? 그래픽 프로그래밍은 정말 매력적인 분야라 재능넷에서도 인기 있는 주제 중 하나야. 그럼 이제 우리가 배울 내용이 얼마나 멋진지 함께 알아보자!
🎨 그래픽 프로그래밍이란?
컴퓨터를 이용해 시각적인 요소를 만들어내는 거야. 게임, 애니메이션, 시뮬레이션 등 우리가 보는 거의 모든 디지털 그래픽이 여기에 해당돼!
자, 이제 본격적으로 Swift와 Metal에 대해 알아보자. 준비됐어? 그럼 고고! 🏃♂️💨
Swift: 애플의 강력한 프로그래밍 언어 🍎
Swift는 애플이 만든 현대적이고 안전한 프로그래밍 언어야. iOS, macOS, watchOS, tvOS 앱을 만들 때 주로 사용되지. Swift는 빠르고, 안전하며, 표현력이 풍부해서 많은 개발자들이 좋아한다구!
🚀 Swift의 특징:
- 빠른 성능
- 안전한 코드 작성 가능
- 현대적이고 깔끔한 문법
- 강력한 타입 추론
- 메모리 관리의 자동화
Swift로 그래픽 프로그래밍을 할 때의 장점은 뭘까? 바로 Metal과의 완벽한 호환성이야! Metal은 애플의 저수준 그래픽 API인데, Swift와 찰떡궁합이라고 할 수 있지.
Swift를 사용하면 복잡한 그래픽 연산도 효율적으로 처리할 수 있어. 게다가 Swift의 문법이 직관적이라 그래픽 알고리즘을 구현하기도 훨씬 쉽지. 재능넷에서 Swift 관련 강의를 들어본 적 있다면, 이런 장점을 직접 느껴봤을 거야!
Swift의 기본을 살펴봤으니, 이제 Metal에 대해 알아볼 차례야. Metal은 그래픽 프로그래밍의 핵심이 될 거야. 준비됐어? 다음 섹션으로 고고! 🏃♀️💨
Metal: 애플의 강력한 그래픽 API 🔧
Metal이 뭔지 궁금해? 간단히 말하면, Metal은 애플이 만든 저수준 그래픽 및 컴퓨팅 API야. '저수준'이라는 말은 하드웨어에 가깝다는 뜻이야. 즉, GPU를 직접적으로 다룰 수 있게 해준다는 거지!
🎮 Metal의 주요 특징:
- 빠른 성능과 낮은 오버헤드
- GPU 가속을 최대한 활용
- 그래픽과 컴퓨팅 작업을 통합
- 애플 플랫폼에 최적화
Metal을 사용하면 정말 멋진 그래픽을 만들 수 있어. 3D 게임, 복잡한 시각화, 고성능 이미지 처리 등 다양한 분야에서 활용할 수 있지. 재능넷에서 Metal 관련 프로젝트를 의뢰하는 경우도 많다던데, 그만큼 수요가 많은 기술이라는 뜻이야!
Metal은 OpenGL ES를 대체하기 위해 만들어졌어. OpenGL ES보다 더 효율적이고 애플 기기에 최적화되어 있지. 그래서 iOS나 macOS 앱을 만들 때 Metal을 사용하면 훨씬 더 좋은 성능을 낼 수 있어.
자, 이제 Metal에 대해 기본적인 이해가 생겼지? 그럼 이제 Swift와 Metal을 어떻게 함께 사용하는지 알아볼 차례야. 다음 섹션에서 본격적으로 Swift와 Metal을 이용한 그래픽 프로그래밍을 시작해볼 거야. 신나지 않아? 😆
Swift와 Metal의 만남: 그래픽 프로그래밍의 시작 🎨
자, 이제 진짜 재미있는 부분이 시작됐어! Swift와 Metal을 함께 사용해서 그래픽 프로그래밍을 하는 방법을 알아볼 거야. 준비됐어? 그럼 시작해보자!
🛠️ 준비물:
- Xcode (최신 버전 추천)
- macOS 디바이스
- Metal을 지원하는 애플 기기 (대부분의 최신 기기들이 지원해)
먼저, Xcode에서 새 프로젝트를 만들어볼까? 'Single View App'을 선택하고, 언어는 당연히 Swift로 설정하자. 프로젝트 이름은 멋지게 'SwiftMetalGraphics'라고 지어볼까?
프로젝트를 만들었으면, 이제 Metal 프레임워크를 import 해야 해. ViewController.swift 파일을 열고 맨 위에 다음 줄을 추가해줘:
import Metal
import MetalKit
이렇게 하면 Metal과 관련된 클래스와 함수들을 사용할 수 있게 돼. 멋지지 않아? 😎
다음으로, Metal 디바이스를 생성해야 해. 이건 그래픽 작업을 수행할 GPU를 나타내는 객체야. ViewController 클래스 안에 다음 코드를 추가해보자:
let device = MTLCreateSystemDefaultDevice()!
이 한 줄로 우리는 시스템의 기본 Metal 디바이스를 가져올 수 있어. 대부분의 경우 이게 컴퓨터나 기기의 주 GPU가 될 거야.
이제 Metal view를 만들어볼 차례야. 이 뷰는 우리가 그린 그래픽을 화면에 표시해줄 거야. viewDidLoad() 메서드 안에 다음 코드를 추가해보자:
let metalView = MTKView(frame: view.bounds, device: device)
metalView.clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0)
view.addSubview(metalView)
이 코드는 Metal view를 생성하고, 배경색을 설정한 다음, 메인 뷰에 추가해. 여기서는 배경색을 예쁜 하늘색으로 설정했어. 마음에 드는 색으로 바꿔봐도 좋아!
우와, 벌써 기본적인 Metal 프로젝트를 만들었어! 이제 우리는 이 프로젝트를 바탕으로 더 복잡하고 멋진 그래픽을 그릴 수 있게 됐어. 재능넷에서 그래픽 프로그래밍 관련 프로젝트를 진행한다면, 이런 기본 설정부터 시작하겠지?
다음 섹션에서는 실제로 뭔가를 그려볼 거야. 간단한 삼각형부터 시작해서 점점 더 복잡한 도형을 그리는 방법을 배워볼 거야. 기대되지 않아? 그럼 계속 가보자! 🚀
첫 번째 도형 그리기: 삼각형의 탄생 🔺
자, 이제 정말 재미있는 부분이 시작됐어! 우리의 첫 번째 도형, 삼각형을 그려볼 거야. 왜 삼각형일까? 그건 삼각형이 가장 기본적인 다각형이기 때문이야. 복잡한 3D 모델도 결국은 수많은 삼각형으로 이루어져 있다구!
🎨 그래픽 파이프라인 기초:
- 정점 데이터 준비
- 정점 셰이더 작성
- 프래그먼트 셰이더 작성
- 렌더 파이프라인 설정
- 그리기 명령 실행
먼저, 삼각형의 정점 데이터를 준비해볼까? 정점은 도형의 꼭지점을 말해. 삼각형은 세 개의 정점으로 이루어져 있지. 다음 코드를 ViewController 클래스에 추가해봐:
let vertices: [Float] = [
0.0, 0.5, 0.0,
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0
]
var vertexBuffer: MTLBuffer!
이 코드는 삼각형의 세 꼭지점 좌표를 정의하고, 이 데이터를 저장할 Metal 버퍼를 선언해. x, y, z 좌표로 이루어진 3D 공간에서의 위치를 나타내는 거야.
다음으로, 이 정점 데이터를 GPU로 보내기 위해 버퍼를 생성해야 해. viewDidLoad() 메서드에 다음 코드를 추가해봐:
let dataSize = vertices.count * MemoryLayout<float>.size
vertexBuffer = device.makeBuffer(bytes: vertices, length: dataSize, options: [])</float>
이제 정점 데이터가 준비됐으니, 셰이더를 작성할 차례야. 셰이더는 GPU에서 실행되는 작은 프로그램이야. 정점 셰이더와 프래그먼트 셰이더, 이 두 가지를 만들어볼 거야.
새 파일을 만들고 이름을 'Shaders.metal'로 지어줘. 그리고 다음 코드를 입력해:
#include <metal_stdlib>
using namespace metal;
vertex float4 vertex_shader(uint vertexID [[vertex_id]],
constant float3 *vertices [[buffer(0)]]) {
return float4(vertices[vertexID], 1);
}
fragment float4 fragment_shader() {
return float4(1, 0, 0, 1);
}</metal_stdlib>
이 코드에서 vertex_shader는 정점의 위치를 결정하고, fragment_shader는 각 픽셀의 색상을 결정해. 여기서는 모든 픽셀을 빨간색으로 설정했어.
와, 벌써 많은 걸 했네! 이제 마지막으로 렌더 파이프라인을 설정하고 그리기 명령을 실행해볼 차례야. ViewController에 다음 프로퍼티들을 추가해줘:
var commandQueue: MTLCommandQueue!
var pipelineState: MTLRenderPipelineState!
그리고 viewDidLoad()에 다음 코드를 추가해:
commandQueue = device.makeCommandQueue()
let library = device.makeDefaultLibrary()!
let vertexFunction = library.makeFunction(name: "vertex_shader")
let fragmentFunction = library.makeFunction(name: "fragment_shader")
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor)
let metalView = view as! MTKView
metalView.delegate = self
마지막으로, MTKViewDelegate를 구현해서 실제로 그리기 작업을 수행할 거야. ViewController 클래스 아래에 다음 extension을 추가해줘:
extension ViewController: MTKViewDelegate {
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor else { return }
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder?.setRenderPipelineState(pipelineState)
renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}
}
우와, 대단해! 이제 실행해보면 화면에 빨간 삼각형이 나타날 거야. 어때, 생각보다 복잡하지? 하지만 이게 바로 그래픽 프로그래밍의 기본이야. 이 과정을 이해하고 나면, 더 복잡한 도형이나 3D 모델도 그릴 수 있게 될 거야.
재능넷에서 그래픽 프로그래밍 관련 프로젝트를 진행한다면, 이런 기본적인 도형 그리기부터 시작해서 점점 더 복잡한 그래픽을 만들어나가겠지? 다음 섹션에서는 이 삼각형을 움직이게 만들어볼 거야. 기대돼? 그럼 계속 가보자! 🚀
움직이는 삼각형: 애니메이션의 시작 🔄
자, 이제 우리의 삼각형에 생명을 불어넣어볼 거야! 정적인 그래픽도 멋지지만, 움직이는 그래픽은 더 멋지잖아? 이번에는 우리의 삼각형을 회전시켜볼 거야. 준비됐어? 그럼 시작해보자!
🎬 애니메이션 기본 원리:
- 시간에 따라 변하는 값 만들기
- 그 값을 이용해 도형의 위치나 모양 변경하기
- 매 프레임마다 새로운 상태 그리기
먼저, 회전 각도를 저장할 변수를 만들자. ViewController 클래스에 다음 프로퍼티를 추가해줘:
var rotationAngle: Float = 0.0
이제 이 각도를 이용해서 삼각형을 회전시킬 거야. 하지만 그전에, 회전 변환을 수행할 함수가 필요해. ViewController 클래스에 다음 함수를 추가해줘:
func rotateVertex(_ vertex: SIMD3<float>, by angle: Float) -> SIMD3<float> {
let x = vertex.x * cos(angle) - vertex.y * sin(angle)
let y = vertex.x * sin(angle) + vertex.y * cos(angle)
return SIMD3<float>(x, y, vertex.z)
}</float></float></float>
이 함수는 2D 평면에서 점을 회전시키는 공식을 구현한 거야. 삼각형의 각 정점에 이 함수를 적용하면 전체 삼각형이 회전하게 될 거야.
이제 draw(in:) 함수를 수정해서 매 프레임마다 삼각형을 조금씩 회전시켜보자:
func draw(in view: MTKView) {
rotationAngle += 0.01 // 매 프레임마다 조금씩 각도 증가
let rotatedVertices: [SIMD3<float>] = [
rotateVertex(SIMD3<float>(0, 0.5, 0), by: rotationAngle),
rotateVertex(SIMD3<float>(-0.5, -0.5, 0), by: rotationAngle),
rotateVertex(SIMD3<float>(0.5, -0.5, 0), by: rotationAngle)
]
// 버퍼 업데이트
vertexBuffer.contents().copyMemory(from: rotatedVertices, byteCount: MemoryLayout<simd3>>.stride * rotatedVertices.count)
guard let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor else { return }
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder?.setRenderPipelineState(pipelineState)
renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}</simd3></float></float></float></float>
와! 이제 실행해보면 삼각형이 천천히 회전하는 걸 볼 수 있을 거야. 멋지지 않아? 이게 바로 애니메이션의 기본 원리야. 매 프레임마다 도형의 상태를 조금씩 변경하고, 그 변경된 상태를 다시 그리는 거지.
이 기술을 응용하면 정말 다양한 애니메이션을 만들 수 있어. 예를 들어, 삼각형을 회전시키는 대신 크기를 변경하거나 위치를 이동시킬 수도 있지. 재능넷에서 그래픽 프로그래밍 관련 프로젝트를 진행한다면, 이런 기본적인 애니메이션 기술이 정말 유용할 거야.
더 나아가서, 여러 개의 도형을 동시에 애니메이션화하거나, 사용자 입력에 따라 애니메이션을 제어할 수도 있어. 예를 들어, 화면을 터치하면 삼각형의 회전 속도가 바뀌게 만들 수 있겠지?
🚀 도전 과제:
- 삼각형의 색상을 시간에 따라 변경해보기
- 여러 개의 삼각형을 동시에 회전시켜보기
- 사용자가 화면을 터치하면 회전 방향이 바뀌게 만들어보기
어때, 그래픽 프로그래밍이 점점 더 재미있어지지 않아? 이제 우리는 기본적인 도형을 그리고, 그걸 움직이게 만드는 방법을 배웠어. 다음 섹션에서는 좀 더 복잡한 도형을 그려볼 거야. 3D 큐브는 어때? 기대되지? 그럼 계속 가보자! 🚀
3D의 세계로: 회전하는 큐브 만들기 🧊
자, 이제 우리의 그래픽 프로그래밍 여행을 2D에서 3D로 확장해볼 시간이야! 회전하는 삼각형도 멋졌지만, 회전하는 3D 큐브는 어떨까? 이번에는 좀 더 복잡하지만, 그만큼 더 멋진 결과물을 만들어볼 거야. 준비됐어? 그럼 시작해보자!
🧱 3D 큐브의 구성 요소:
- 8개의 정점
- 6개의 면 (각 면은 2개의 삼각형으로 구성)
- 3D 공간에서의 회전 (x, y, z 축)
먼저, 큐브의 정점 데이터를 정의해보자. ViewController 클래스에 다음 프로퍼티를 추가해줘:
let vertices: [SIMD3<float>] = [
SIMD3<float>(-1, -1, 1), SIMD3<float>(1, -1, 1), SIMD3<float>(1, 1, 1), SIMD3<float>(-1, 1, 1),
SIMD3<float>(-1, -1, -1), SIMD3<float>(1, -1, -1), SIMD3<float>(1, 1, -1), SIMD3<float>(-1, 1, -1)
]
let indices: [UInt16] = [
0, 1, 2, 2, 3, 0, // front
1, 5, 6, 6, 2, 1, // right
5, 4, 7, 7, 6, 5, // back
4, 0, 3, 3, 7, 4, // left
3, 2, 6, 6, 7, 3, // top
4, 5, 1, 1, 0, 4 // bottom
]
var vertexBuffer: MTLBuffer!
var indexBuffer: MTLBuffer!</float></float></float></float></float></float></float></float></float>
이 코드는 큐브의 8개 정점과, 이 정점들을 이용해 6개의 면을 구성하는 방법을 정의해. indices 배열은 각 삼각형을 어떤 정점으로 구성할지 알려주는 거야.
이제 이 데이터를 GPU로 보내기 위한 버퍼를 생성해야 해. viewDidLoad() 메서드에 다음 코드를 추가해줘:
vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<simd3>>.stride * vertices.count, options: [])
indexBuffer = device.makeBuffer(bytes: indices, length: MemoryLayout<uint16>.stride * indices.count, options: [])</uint16></simd3>
다음으로, 3D 회전을 위한 행렬을 만들어야 해. 다음 함수들을 ViewController 클래스에 추가해줘:
func rotateX(_ angle: Float) -> simd_float4x4 {
let c = cos(angle)
let s = sin(angle)
return simd_float4x4(
SIMD4<float>(1, 0, 0, 0),
SIMD4<float>(0, c, -s, 0),
SIMD4<float>(0, s, c, 0),
SIMD4<float>(0, 0, 0, 1)
)
}
func rotateY(_ angle: Float) -> simd_float4x4 {
let c = cos(angle)
let s = sin(angle)
return simd_float4x4(
SIMD4<float>(c, 0, s, 0),
SIMD4<float>(0, 1, 0, 0),
SIMD4<float>(-s, 0, c, 0),
SIMD4<float>(0, 0, 0, 1)
)
}
func rotateZ(_ angle: Float) -> simd_float4x4 {
let c = cos(angle)
let s = sin(angle)
return simd_float4x4(
SIMD4<float>(c, -s, 0, 0),
SIMD4<float>(s, c, 0, 0),
SIMD4<float>(0, 0, 1, 0),
SIMD4<float>(0, 0, 0, 1)
)
}</float></float></float></float></float></float></float></float></float></float></float></float>
이제 셰이더를 수정해서 3D 변환을 적용할 수 있게 만들어보자. Shaders.metal 파일을 다음과 같이 수정해:
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
float3 position [[attribute(0)]];
};
struct VertexOut {
float4 position [[position]];
float4 color;
};
vertex VertexOut vertex_shader(const VertexIn vertex_in [[stage_in]],
constant float4x4 &modelViewProjection [[buffer(1)]]) {
VertexOut vertex_out;
vertex_out.position = modelViewProjection * float4(vertex_in.position, 1.0);
vertex_out.color = float4(abs(vertex_in.position), 1.0); // 위치에 따라 색상 변경
return vertex_out;
}
fragment float4 fragment_shader(VertexOut interpolated [[stage_in]]) {
return interpolated.color;
}</metal_stdlib>
이 셰이더는 각 정점의 위치를 3D 공간에서 변환하고, 위치에 따라 색상을 지정해. 결과적으로 우리의 큐브는 다양한 색상으로 표현될 거야!
마지막으로, draw(in:) 함수를 수정해서 우리의 3D 큐브를 그리고 회전시켜보자:
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor else { return }
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder?.setRenderPipelineState(pipelineState)
renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
let angle = Float(CACurrentMediaTime())
let rotationMatrix = rotateY(angle) * rotateX(angle * 0.5)
renderEncoder?.setVertexBytes(&rotationMatrix, length: MemoryLayout<simd_float4x4>.stride, index: 1)
renderEncoder?.drawIndexedPrimitives(type: .triangle,
indexCount: indices.count,
indexType: .uint16,
indexBuffer: indexBuffer,
indexBufferOffset: 0)
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}</simd_float4x4>
우와, 정말 대단해! 이제 실행해보면 화려한 색상의 3D 큐브가 회전하는 걸 볼 수 있을 거야. 이게 바로 3D 그래픽의 기본이야. 이 기술을 응용하면 더 복잡한 3D 모델도 만들 수 있어.
재능넷에서 3D 그래픽 프로그래밍 관련 프로젝트를 진행한다면, 이런 기본적인 3D 렌더링 기술이 정말 유용할 거야. 게임 개발, 3D 시각화, AR/VR 애플리케이션 등 다양한 분야에서 이 기술을 활용할 수 있지.
🚀 다음 단계 도전 과제:
- 큐브에 텍스처 입히기
- 광원 효과 추가하기
- 여러 개의 큐브를 동시에 렌더링하기
- 사용자 입력으로 큐브 조작하기
어때, 3D 그래픽의 세계는 정말 흥미진진하지 않아? 우리는 이제 2D 도형부터 3D 큐브까지 만들어봤어. 이게 바로 그래픽 프로그래밍의 매력이야. 상상력만 있다면 무엇이든 만들어낼 수 있지!
다음 섹션에서는 우리가 만든 3D 큐브에 텍스처를 입혀볼 거야. 실제 물체처럼 보이게 만드는 거지. 기대되지 않아? 그럼 계속 가보자! 🚀
현실감 더하기: 3D 큐브에 텍스처 입히기 🖼️
자, 이제 우리의 3D 큐브를 더욱 멋지게 만들어볼 시간이야! 단색의 큐브도 멋졌지만, 실제 물체처럼 보이게 하려면 텍스처가 필요해. 텍스처를 입히면 큐브가 마치 나무 상자나 금속 큐브처럼 보일 수 있지. 준비됐어? 그럼 시작해보자!
🎨 텍스처 매핑의 기본 단계:
- 텍스처 이미지 준비
- 텍스처 좌표 설정
- 셰이더에서 텍스처 샘플링
- 최종 색상 계산
먼저, 텍스처로 사용할 이미지를 프로젝트에 추가해야 해. 간단한 나무 텍스처 이미지를 Assets.xcassets에 추가해줘. 이름은 'wood_texture'라고 하자.
다음으로, 텍스처 좌표를 정의해야 해. ViewController 클래스에 다음 프로퍼티를 추가해줘:
let textureCoordinates: [SIMD2<float>] = [
SIMD2<float>(0, 1), SIMD2<float>(1, 1), SIMD2<float>(1, 0), SIMD2<float>(0, 0),
SIMD2<float>(0, 1), SIMD2<float>(1, 1), SIMD2<float>(1, 0), SIMD2<float>(0, 0)
]
var textureCoordinateBuffer: MTLBuffer!</float></float></float></float></float></float></float></float></float>
이 텍스처 좌표는 각 정점이 텍스처의 어느 부분과 대응되는지를 나타내. (0,0)은 텍스처의 왼쪽 아래 모서리, (1,1)은 오른쪽 위 모서리를 의미해.
이제 텍스처 좌표를 위한 버퍼를 생성하고, 텍스처를 로드해야 해. viewDidLoad() 메서드에 다음 코드를 추가해줘:
textureCoordinateBuffer = device.makeBuffer(bytes: textureCoordinates, length: MemoryLayout<simd2>>.stride * textureCoordinates.count, options: [])
let textureLoader = MTKTextureLoader(device: device)
let texture = try! textureLoader.newTexture(name: "wood_texture", scaleFactor: 1.0, bundle: nil, options: nil)</simd2>
다음으로, 셰이더를 수정해서 텍스처를 사용할 수 있게 만들어보자. Shaders.metal 파일을 다음과 같이 수정해:
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
float3 position [[attribute(0)]];
float2 texCoord [[attribute(1)]];
};
struct VertexOut {
float4 position [[position]];
float2 texCoord;
};
vertex VertexOut vertex_shader(const VertexIn vertex_in [[stage_in]],
constant float4x4 &modelViewProjection [[buffer(1)]]) {
VertexOut vertex_out;
vertex_out.position = modelViewProjection * float4(vertex_in.position, 1.0);
vertex_out.texCoord = vertex_in.texCoord;
return vertex_out;
}
fragment float4 fragment_shader(VertexOut interpolated [[stage_in]],
texture2d<float> texture [[texture(0)]]) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
return texture.sample(textureSampler, interpolated.texCoord);
}</float></metal_stdlib>
이 셰이더는 각 픽셀의 색상을 텍스처에서 샘플링해서 결정해. 결과적으로 우리의 큐브는 나무 텍스처로 덮히게 될 거야!
마지막으로, draw(in:) 함수를 수정해서 텍스처를 적용해보자:
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor else { return }
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder?.setRenderPipelineState(pipelineState)
renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder?.setVertexBuffer(textureCoordinateBuffer, offset: 0, index: 1)
let angle = Float(CACurrentMediaTime())
let rotationMatrix = rotateY(angle) * rotateX(angle * 0.5)
renderEncoder?.setVertexBytes(&rotationMatrix, length: MemoryLayout<simd_float4x4>.stride, index: 2)
renderEncoder?.setFragmentTexture(texture, index: 0)
renderEncoder?.drawIndexedPrimitives(type: .triangle,
indexCount: indices.count,
indexType: .uint16,
indexBuffer: indexBuffer,
indexBufferOffset: 0)
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}</simd_float4x4>
우와, 정말 대단해! 이제 실행해보면 나무 텍스처가 입혀진 3D 큐브가 회전하는 걸 볼 수 있을 거야. 이게 바로 텍스처 매핑의 마법이야. 이 기술을 응용하면 어떤 물체든 원하는 질감으로 표현할 수 있어.
재능넷에서 3D 그래픽이나 게임 개발 관련 프로젝트를 진행한다면, 이런 텍스처 매핑 기술이 정말 중요할 거야. 게임의 캐릭터, 배경, 아이템 등 모든 3D 객체에 이 기술이 사용되거든.
🚀 다음 단계 도전 과제:
- 큐브의 각 면에 다른 텍스처 적용해보기
- 노멀 맵을 이용해 입체감 더하기
- 환경 맵을 이용한 반사 효과 구현하기
- 사용자 입력으로 텍스처 변경하기
어때, 텍스처를 입힌 3D 큐브는 훨씬 더 멋지지 않아? 이제 우리는 2D 도형부터 시작해서 텍스처가 입혀진 3D 객체까지 만들어봤어. 이게 바로 그래픽 프로그래밍의 힘이야. 상상력만 있다면 어떤 가상 세계든 만들어낼 수 있지!
다음 섹션에서는 우리의 3D 씬에 광원 효과를 추가해볼 거야. 빛과 그림자로 더욱 사실적인 3D 환경을 만드는 거지. 기대되지 않아? 그럼 계속 가보자! 🚀
빛의 마법: 3D 씬에 광원 효과 추가하기 💡
와, 여기까지 정말 대단한 여정이었어! 우리는 이제 텍스처가 입혀진 3D 큐브를 만들 수 있게 됐지. 하지만 뭔가 빠진 것 같지 않아? 바로 빛이야! 실제 세계의 물체들은 빛에 의해 밝게 빛나기도 하고, 그림자를 만들기도 해. 이번에는 우리의 3D 씬에 광원 효과를 추가해서 더욱 사실적으로 만들어볼 거야. 준비됐어? 그럼 시작해보자!
🌟 광원 효과의 기본 요소:
- 주변광 (Ambient Light)
- 확산광 (Diffuse Light)
- 반사광 (Specular Light)
- 노멀 벡터 (Normal Vector)
먼저, 우리의 3D 모델에 노멀 벡터를 추가해야 해. 노멀 벡터는 표면의 방향을 나타내는 벡터로, 빛이 어떻게 반사될지 계산하는 데 사용돼. ViewController 클래스에 다음 프로퍼티를 추가해줘:
let normals: [SIMD3<float>] = [
SIMD3<float>(0, 0, 1), SIMD3<float>(0, 0, 1), SIMD3<float>(0, 0, 1), SIMD3<float>(0, 0, 1),
SIMD3<float>(0, 0, -1), SIMD3<float>(0, 0, -1), SIMD3<float>(0, 0, -1), SIMD3<float>(0, 0, -1),
SIMD3<float>(1, 0, 0), SIMD3<float>(1, 0, 0), SIMD3<float>(1, 0, 0), SIMD3<float>(1, 0, 0),
SIMD3<float>(-1, 0, 0), SIMD3<float>(-1, 0, 0), SIMD3<float>(-1, 0, 0), SIMD3<float>(-1, 0, 0),
SIMD3<float>(0, 1, 0), SIMD3<float>(0, 1, 0), SIMD3<float>(0, 1, 0), SIMD3<float>(0, 1, 0),
SIMD3<float>(0, -1, 0), SIMD3<float>(0, -1, 0), SIMD3<float>(0, -1, 0), SIMD3<float>(0, -1, 0)
]
var normalBuffer: MTLBuffer!</float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float></float>
이 노멀 벡터들은 큐브의 각 면이 어느 방향을 향하고 있는지 나타내. 이걸 이용해서 빛이 어떻게 반사될지 계산할 수 있어.
다음으로, 광원의 위치와 색상을 정의해야 해. ViewController 클래스에 다음 구조체와 프로퍼티를 추가해줘:
struct Light {
var position: SIMD3<float>
var color: SIMD3<float>
var ambientIntensity: Float
var diffuseIntensity: Float
var specularIntensity: Float
}
let light = Light(position: SIMD3<float>(2, 2, 2),
color: SIMD3<float>(1, 1, 1),
ambientIntensity: 0.1,
diffuseIntensity: 0.8,
specularIntensity: 0.5)</float></float></float></float>
이제 셰이더를 수정해서 광원 효과를 계산할 수 있게 만들어보자. Shaders.metal 파일을 다음과 같이 수정해:
#include <metal_stdlib>
using namespace metal;
struct VertexIn {
float3 position [[attribute(0)]];
float2 texCoord [[attribute(1)]];
float3 normal [[attribute(2)]];
};
struct VertexOut {
float4 position [[position]];
float2 texCoord;
float3 normal;
float3 fragPos;
};
struct Light {
float3 position;
float3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity;
};
vertex VertexOut vertex_shader(const VertexIn vertex_in [[stage_in]],
constant float4x4 &modelMatrix [[buffer(1)]],
constant float4x4 &viewProjectionMatrix [[buffer(2)]]) {
VertexOut vertex_out;
float4 worldPosition = modelMatrix * float4(vertex_in.position, 1.0);
vertex_out.position = viewProjectionMatrix * worldPosition;
vertex_out.texCoord = vertex_in.texCoord;
vertex_out.normal = (modelMatrix * float4(vertex_in.normal, 0.0)).xyz;
vertex_out.fragPos = worldPosition.xyz;
return vertex_out;
}
fragment float4 fragment_shader(VertexOut interpolated [[stage_in]],
texture2d<float> texture [[texture(0)]],
constant Light &light [[buffer(0)]],
constant float3 &cameraPosition [[buffer(1)]]) {
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear);
float4 textureColor = texture.sample(textureSampler, interpolated.texCoord);
float3 normal = normalize(interpolated.normal);
float3 lightDir = normalize(light.position - interpolated.fragPos);
float3 viewDir = normalize(cameraPosition - interpolated.fragPos);
float3 reflectDir = reflect(-lightDir, normal);
float3 ambient = light.ambientIntensity * light.color;
float diff = max(dot(normal, lightDir), 0.0);
float3 diffuse = light.diffuseIntensity * diff * light.color;
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
float3 specular = light.specularIntensity * spec * light.color;
float3 result = (ambient + diffuse + specular) * textureColor.rgb;
return float4(result, textureColor.a);
}</float></metal_stdlib>
이 셰이더는 주변광, 확산광, 반사광을 모두 계산해서 최종 색상을 결정해. 결과적으로 우리의 큐브는 빛에 의해 밝게 빛나고 그림자도 생기게 될 거야!
마지막으로, draw(in:) 함수를 수정해서 광원 정보와 카메라 위치를 셰이더에 전달해야 해:
func draw(in view: MTKView) {
guard let drawable = view.currentDrawable,
let renderPassDescriptor = view.currentRenderPassDescriptor else { return }
let commandBuffer = commandQueue.makeCommandBuffer()
let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
renderEncoder?.setRenderPipelineState(pipelineState)
renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
renderEncoder?.setVertexBuffer(textureCoordinateBuffer, offset: 0, index: 1)
renderEncoder?.setVertexBuffer(normalBuffer, offset: 0, index: 2)
let angle = Float(CACurrentMediaTime())
let modelMatrix = rotateY(angle) * rotateX(angle * 0.5)
let viewMatrix = matrix4x4_translation(0, 0, -5)
let projectionMatrix = matrix4x4_perspective_projection(aspect: Float(view.drawableSize.width / view.drawableSize.height), fovy: Float.pi / 3, near: 0.1, far: 100)
let viewProjectionMatrix = matrix_multiply(projectionMatrix, viewMatrix)
renderEncoder?.setVertexBytes(&modelMatrix, length: MemoryLayout<simd_float4x4>.stride, index: 1)
renderEncoder?.setVertexBytes(&viewProjectionMatrix, length: MemoryLayout<simd_float4x4>.stride, index: 2)
renderEncoder?.setFragmentBytes(&light, length: MemoryLayout<light>.stride, index: 0)
let cameraPosition = SIMD3<float>(0, 0, -5)
renderEncoder?.setFragmentBytes(&cameraPosition, length: MemoryLayout<simd3>>.stride, index: 1)
renderEncoder?.setFragmentTexture(texture, index: 0)
renderEncoder?.drawIndexedPrimitives(type: .triangle,
indexCount: indices.count,
indexType: .uint16,
indexBuffer: indexBuffer,
indexBufferOffset: 0)
renderEncoder?.endEncoding()
commandBuffer?.present(drawable)
commandBuffer?.commit()
}</simd3></float></light></simd_float4x4></simd_float4x4>
우와, 정말 대단해! 이제 실행해보면 빛에 의해 밝게 빛나고 그림자가 생긴 3D 큐브를 볼 수 있을 거야. 이게 바로 광원 효과의 마법이야. 이 기술을 응용하면 훨씬 더 사실적인 3D 씬을 만들 수 있어.
재능넷에서 3D 그래픽이나 게임 개발 관련 프로젝트를 진행한다면, 이런 광원 효과 기술이 정말 중요할 거야. 게임의 분위기, 캐릭터의 입체감, 환경의 현실감 등을 모두 이 기술로 만들어낼 수 있거든.
🚀 다음 단계 도전 과제:
- 여러 개의 광원 추가하기
- 그림자 렌더링 구현하기
- 법선 매핑(Normal Mapping) 적용하기
- 사용자 입력으로 광원 조작하기
어때, 광원 효과를 추가한 3D 씬은 훨씬 더 멋지지 않아? 이제 우리는 2D 도형부터 시작해서 빛과 그림자가 있는 3D 씬까지 만들어봤어. 이게 바로 그래픽 프로그래밍의 매력이야. 상상력만 있다면 어떤 가상 세계든 만들어낼 수 있지!
여기까지 오느라 정말 수고 많았어. 우리는 Swift와 Metal을 이용해서 정말 멋진 3D 그래픽을 만들어냈어. 이제 너는 기본적인 3D 그래픽 프로그래밍 기술을 모두 배웠어. 이걸 바탕으로 더 복잡하고 아름다운 3D 세계를 만들어낼 수 있을 거야. 계속해서 실험하고 새로운 것을 시도해봐. 그래픽 프로그래밍의 세계는 정말 무궁무진하거든!
그래픽 프로그래밍 여행이 즐거웠길 바라. 앞으로도 계속 멋진 것들을 만들어내길 응원할게. 화이팅! 🚀✨