koodev

OpenGL로 원 그리기

Programming

OpenGL로 원을 그리려면 어떻게 해야 할까? OpenGL primitive에는 원이 없고 점이나 선, 삼각형 등만 그릴 수 있다. 그러면 점근적인 접근방식으로 삼각형을 여러개 이어서 원 모양으로 그릴 수 있을 것이다. 그렇지만 이 방식은 정교하게 하려하면 할수록 더 많은 점들이 필요하다. 여기서는 점 4개 만으로 그릴 수 있는 방법을 정리해 보았다.

우선 그릴 원을 정의해 보자. 구현을 단순하게 하기 위해서 NDC(Normalized Device Coordinates)를 기반으로 해서 아래와 같이 정의해 보았다.

  • 반지름: 0.5
  • 원점좌표: (0.0, 0.0, 0.0)
  • 색깔: 외접하는 사각형의 네 귀퉁이를 빨강, 파랑, 노랑, 초록으로 하여 섞어서 보간

추가적으로 타원이 아닌 동그라미를 그리기 위해서는 그림을 그릴 윈도우의 가로세로 비율이 필요하다. 가로/세로 = 3/2 = 1.5 로 고정시킨다.

일단 원을 그리기 앞서 외접하는 사각형을 그린다. 점들의 좌표와 인덱스는 아래와 같이 정의한다.

let vertices: [GLfloat] = [
    -0.5, -0.5, +0.0, 1.0, 0.0, 0.0,
    +0.5, -0.5, +0.0, 0.0, 1.0, 0.0,
    +0.5, +0.5, +0.0, 0.0, 0.0, 1.0,
    -0.5, +0.5, +0.0, 1.0, 1.0, 0.0,
]
let indices: [GLuint] = [
    0, 1, 2,
    0, 2, 3
]

아래는 Vertex shader고,

layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
out vec3 ourColor;
void main()
{
  gl_Position = vec4(position.x, position.y, position.z, 1.0);
  ourColor = color;
}

Fragment shader는 아래와 같다.

#version 330 core
in vec3 ourColor;
out vec4 color;
void main()
{
  color = vec4(ourColor, 1.0);
}

이걸 가지고 사각형을 그리면 아래와 같이 된다. 풀 소스는 다음 링크를 참고한다:

결과는 아래 그림과 같다.

이를 기반으로 원을 그려보자. 원을 그리는 전략은 각 Fragment에서 원의 중심 좌표로부터 거리를 재서 정의한 반지름 안에 들어오면 해당하는 색을 칠하고, 거리가 너무 멀면 색을 칠하지 않는 것이다. 거리를 재기 위해서 GLSL의 내장 함수인 distance를, 반지름과 비교하여 0아님 1의 값을 내기 위해 step함수를 사용할 것이다.

우선 Vertex shader는 아래와 같다.

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
out vec3 ourColor;
out vec3 ourPosition;
void main()
{
  gl_Position = vec4(position.x, position.y, position.z, 1.0);
  ourColor = color;
  float ratio = 3.0 / 2.0;
  ourPosition = vec3(position.x * ratio, position.y, position.z);
}

그리고 Fragment shader는 아래와 같다.

#version 330 core
in vec3 ourColor;
in vec3 ourPosition;
out vec4 color;
void main()
{
  float radius = 0.5;
  vec3 origin = vec3(0.0);
  float dist = distance(origin, ourPosition);
  float alpha = step(dist, radius);
  color = vec4(ourColor, alpha);
}

그러면 아래와 같은 동그라미를 그릴 수 있다. 근데 원의 가장자리가 찌글찌글한 모양새를 (선호하는 사람도 있겠지만) 불편해 하는 사람도 있을 것이다.

가장자리에 안티알리아스 효과를 주기 위해서 step 함수를 대신하여 smoothstep 함수를 사용할 수 있다. step이 계단이라면 smoothstep은 언덕길이라고 할 수 있겠다. 아래 그림 참고.

smoothstep을 사용하는 Fragment shader는 아래와 같다.

#version 330 core
in vec3 ourColor;
in vec3 ourPosition;
out vec4 color;
void main()
{
  float radius = 0.5;
  vec3 origin = vec3(0.0);
  float dist = distance(origin, ourPosition);
  float delta = 0.01;
  float alpha = smoothstep(dist-delta, dist, radius-delta);
  color = vec4(ourColor, alpha);
}

그리고 최종 결과는 아래와 같다.

풀 소스는 다음 링크를 참고한다: CircleRenderer.swift

참고한 사이트:

Swift3 - result unused warning 없애기

Programming

+= 연산자를 재정의하여 쓰는데 "Result of operator '+=' is unused" 라는 warning이 발생하여 이걸 없애는 방법을 좀 찾아보았다. 결론부터 말하자면, @discardableResult 을 함수 앞에 붙여주면 해결된다.

GLKit 모듈에 붙어있는 GLKVector3에 곱셈과 +=, -= 등의 연산자가 없어서 아래와 같이 연산자 재정의를 해 보았다.

근데 막상 이걸 사용하려 했더니, 아래와 같이 warning이 떴다. 무시해도 괜찮긴 하지만 영 찜찜하다.

관련해서 찾아보니, Swift 2점대 에서는 '@warn_unused_result' 라는 지시자가 있어서 리턴값을 사용하지 않으면 컴파일러가 warning을 내뱉게 하고 있었다. 즉, 이전까지는 기본적으로는 리턴값을 쓰지 않아도 warning이 뜨지 않았다는 말이다. 그리고 Swift 3점대에 오면서 이게 바뀌어서 리턴값을 쓰지 않으면 컴파일러가 warning을 내뿜도록 하고 있다.

unused warning을 없애기 위한 방법은 두 가지가 있다.

  • @discardableResult 을 함수 앞에 붙여주거나,
  • 함수(연산자) 호출 단에서 리턴값을 밑줄(_)에 할당하는 표현식을 쓰는 것이다. 즉, _ = self.position += self.front * velocity 요렇게 쓰면 되겠지.

참고: https://useyourloaf.com/blog/swift-3-warning-of-unused-result/

'Programming' 카테고리의 다른 글

macOS에 emacs ggtags 설치 및 설정  (0) 2017.10.17
Xcode에 assimp 올리기  (0) 2017.06.06
OpenGL로 원 그리기  (1) 2017.05.27
Swift - 튜플에 포인터로 접근하기  (0) 2017.05.14
Unsigned Integer to String with Generics  (0) 2017.04.23

Swift - 튜플에 포인터로 접근하기

Programming

상황은 이렇다. GLKMatrix4glUniformMatrix4fv()를 사용하려 한다. 문제는 GLKMatrix4가 Float 배열로 이루어진 게 아니라 16개짜리 튜플로 이루어져 있다는 점이다. 배열로 된 경우 자동으로 C포인터로 변환되어 C API에다 사용할 수 있지만 튜플일 경우 그럴 수가 없다. 참고로 여기를 보면 과거에는 GLKMatrix4의 내부 데이터가 튜플이 아니라 배열이었던 것 같다. 참고로 현재 내가 사용중인 Swift 버전은 3.1 이다.

즉, 아래와 같이 하면 컴파일 에러가 난다는게 문제다.

var trans: GLKMatrix4 = GLKMatrix4Identity
// trans에 조작 blah blah...
glUniformMatrix4fv(self.transformLoc, 1, GLboolean(GL_FALSE), &trans.m)

찾아낸 방법은 ios - pass a 4x4 matrix to glUniformMatrix4fv 이다. 요약하면 우선 튜플을 포인터로 변환하고(withUnsafePointer), 그 포인터를 다시 Float 포인터로 변환하는 것이다(withMemoryRebound).

withUnsafePointer는 입력된 변수를 그 타입의 포인터로 변환하여 클로저로 넘겨준다. 그리고 변환된 포인터는 클로저 밖에서는 쓸 수 없다.

withUnsafePointer(to: &trans.m) {
// 이제 $0로 넘어온 포인터로 뭔가를 조작하자
}

클로저 안으로 넘어온 인자의 타입은 UnsafePointer<(Float, Float,...)> 이다(튜플의 포인터: Pointee가 튜플임). 그런데 사용하려는 API의 입력 타입은 UnsafePointer<Float> 이므로 또 다시 변환해주어야 한다.

어떤 포인터를 다른 Pointee 타입의 포인터로 변환하기 위해 withMemoryRebound 메소드를 사용한다. 접근 가능한 갯수도 지정해 줄 수 있는데 원래는 접근할 갯수(16)를 정확히 넣어주어야 하겠지만 API가 포인터 하나만 쓸 것이므로 1을 주어도 상관없다. 모두 정리하면 아래와 같이 쓰면 동작한다.

withUnsafePointer(to: &trans.m) {
    $0.withMemoryRebound(to: Float.self, capacity: 16) {
        glUniformMatrix4fv(self.transformLoc, 1, GLboolean(GL_FALSE), $0)
    }
}

위와 같이 써도 좋지만 조금 더 직관적인 방법이 있다. 위에서 튜플 데이터에 대한 포인터를 만들기 위해 withUnsafePointer()를 썼는데, 이는 튜플 데이터를 바로 UnsafePointer 형태로 만드는게 불가능해서였다. 그런데 대상 데이터 변수를 UnsafeMutablePointer로 감싸는 것은 가능하다.

UnsafeMutablePointer는 UnsafeRawPointer로 변환할 수 있는데 UnsafeRawPointer는 bindMemory() 메소드를 사용할 수 있다. bindMemory()는 원하는 데이터형(Pointee)의 포인터로 바꾸어준다. 즉, 아래와 같이도 쓸 수 있다. 블록 없이 쓸 수 있어서 개인적으로 이 방식이 더 마음에 든다.

let m = UnsafeMutablePointer(&trans.m)
let p = UnsafeRawPointer(m).bindMemory(to: Float.self, capacity: 16)
glUniformMatrix4fv(self.transformLoc, 1, GLboolean(GL_FALSE), p)

물론 이걸 with절로 묶어서 쓸 수도 있다. 그런데 with절로 묶었을 때 들어오는 포인터(주소)값이 그냥 UnsafeMutablePointer로 감쌌을 경우의 포인터값과 다른 것으로 보아 with절을 쓰면 카피가 일어나는 것 같다.

withUnsafeMutablePointer(to: &trans.m) {
    m in
    let p = UnsafeRawPointer(m).bindMemory(to: Float.self, capacity: 16)
    glUniformMatrix4fv(self.transformLoc, 1, GLboolean(GL_FALSE), p)
}

'Programming' 카테고리의 다른 글

macOS에 emacs ggtags 설치 및 설정  (0) 2017.10.17
Xcode에 assimp 올리기  (0) 2017.06.06
OpenGL로 원 그리기  (1) 2017.05.27
Swift3 - result unused warning 없애기  (0) 2017.05.23
Unsigned Integer to String with Generics  (0) 2017.04.23

Unsigned Integer to String with Generics

Programming

Swift를 사용해서 부호없는 십진수를 십육진수 형태의 문자열로 바꾸는 Generics 함수를 작성한 내용을 기록한다.

Swift의 포인터를 공부하다가 십진수를 십육진수로 바꾸어 1바이트 단위로 보는 기능이 필요했다. 인터넷을 참고하여 String 클래스의 메소드를 사용하면 Decimal을 Hex로 바꿀 수 있었다.

http://stackoverflow.com/questions/24229505/how-to-convert-an-int-to-hex-string-in-swift

let dec = 10
let hex = String(dec, radix:16)
print("0x\(hex)") // 0xa

그런데 매번 이렇게 정수를 문자열로 바꾸어주는 표현식을 적기가 귀찮아서 함수로 만들어 보기로 했다.

func toHex(_ num: Int) -> String {
    let hex = String(num, radix: 16)
    return "0x\(hex)"
}

let dec = 10
print("\(toHex(dec))") // 0xa

이 함수를 가지고 아래와 같이 1바이트 부호없는 정수와 8바이트 부호없는 정수의 값을 찍어보았다. 당연히 타입이 안 맞으니 에러가 나왔다.

let uint8Pointer = UnsafeMutablePointer<UInt8>.allocate(capacity: 8)
uint8Pointer.initialize(from: [0x37, 0x77, 0x11, 0x11, 0x02, 0x33, 0x39, 0x00])

let uint64Pointer = UnsafeMutableRawPointer(uint8Pointer).bindMemory(to: UInt64.self, capacity: 1)

let rawPointer = UnsafeMutableRawPointer(uint64Pointer)
var fullInteger = rawPointer.load(as: UInt64.self)
var firstByte = rawPointer.load(as: UInt8.self)

func toHex(_ num: UInt64) -> String {
    let hex = String(num, radix: 16)
    return "0x\(hex)"
}

print("8byte: \(toHex(fullInteger))")
print("1byte: \(toHex(firstByte))")

./pointer_test3.swift:22:23: error: cannot convert value of type 'UInt64' to expected argument type 'Int'
print("8byte: \(toHex(fullInteger))")
                      ^~~~~~~~~~~
                      Int(       )
./pointer_test3.swift:23:23: error: cannot convert value of type 'UInt8' to expected argument type 'Int'
print("1byte: \(toHex(firstByte))")
                      ^~~~~~~~~
                      Int(     )

그럼 타입을 맞춰주면 된다. 전달인자 목록의 Int 대신에 UInt를 넣어서 다시 해본다. 그렇지만 역시 에러가 나온다. UInt가 내부적으로 4바이트이면 UInt8, UInt64 어느 쪽과도 타입이 맞지 않는다. 그러면 타입을 맞추기 위해 함수를 2개 만들어야 할까? 2바이트, 4바이트 데이터에 대응하기 위해서는 4개를 만들어야 한다.

코드중복 문제를 해결하기 위해 Generics 라는걸 써보기로 했다. 'Generics는 C++ 템플릿 비슷한 것이니 전달인자 타입을 대충 T로 우겨 넣고 컴파일하면 되겠지'라고 생각하고 함수를 다시 작성해 보았다.

func toHex<T>(_ num: T) -> String {
    let hex = String(num, radix: 16)
    return "0x\(hex)"
}

./pointer_test3.swift:18:15: error: cannot invoke initializer for type 'String' with an argument list of type '(T, radix: Int)'
    let hex = String(num, radix: 16)

./pointer_test3.swift:18:15: note: expected an argument list of type '(T, radix: Int, uppercase: Bool)'
    let hex = String(num, radix: 16)

이번에는 String 클래스의 생성자가 동작을 안했다. 함수 밖에서와 같은 내용일텐데 함수 안에서는 제대로 동작하지 않는 걸까? Swift의 Generics는 C++의 템플릿과는 달리 타입 체킹을 더 엄격히 할 수 있다고 한다. T로 정의된 타입이 들어와도 이게 String 클래스의 생성자에 정의된 제약 조건에 의해서 아무 T나 받지 못하게 되어 있던 것이다.

https://developer.apple.com/reference/swift/string/1641688-init

String 클래스의 해당 생성자 문서를 보면 메소드가 아래와 같이 where로 지정된 제약 조건이 걸려 있다.

init<T>(_ value: T, radix: Int = default, uppercase: Bool = default) where T : UnsignedInteger

즉, T 타입은 아무 타입이 아니라 UnsignedInteger 여야 하는 것이다. 따라서 toHex 함수에도 똑같이 제약 조건을 달아주어야 한다.

func toHex<T>(_ num: T) -> String where T : UnsignedInteger {
    let hex = String(num, radix: 16)
    return "0x\(hex)"
}

print("8byte: \(toHex(fullInteger))") // 8byte: 0x39330211117737
print("1byte: \(toHex(firstByte))") // 1byte: 0x37

그러면 부호있는 정수를 Hex로 바꾸려면 어떻게 해야할까? 아쉽게도 이 경우는 함수를 하나 더 작성해야 한다. UnsignedInteger를 _SignedInteger 라는 제약 조건으로 대체하여 같은 함수를 하나 더 만들면 된다. String 생성자도 이런 코드 중복이 보인다. 어쩔 수 없는 듯.

https://developer.apple.com/reference/swift/string/1640980-init

'Programming' 카테고리의 다른 글

macOS에 emacs ggtags 설치 및 설정  (0) 2017.10.17
Xcode에 assimp 올리기  (0) 2017.06.06
OpenGL로 원 그리기  (1) 2017.05.27
Swift3 - result unused warning 없애기  (0) 2017.05.23
Swift - 튜플에 포인터로 접근하기  (0) 2017.05.14