원문 – Apple Pencil Tutorial: Getting Started

Swift 5 버전에 맞춰 원문을 변경하였습니다.


많은 분들이 멋진 새 iPad Pro를 구입하고 함께 사용하기 위해 Apple Pencil을 가지고 있다는 것을 알고 있습니다.

Apple Pencil로 그림을 그리는 것이 얼마나 멋진지 경험했다면 당신은 모든 앱에 Apple Pencil 지원을 포함하고 싶을 것입니다.

저는 원래 iPad를 구입한 이후로 이 장치와 같은 것을 기다리고 있었습니다. 제가 그린 낙서에서 알 수 있듯이 저는 렘브란트(화가)가 아니지만 연필이 메모를 하는 데에도 좋다는 것을 알게 되었습니다. Apple Pencil이 있는 지금 사람들은 어떤 놀라운 예술 작품을 만들지 상상이 되지 않습니다.

이 Apple Pencil 튜토리얼에서는 Apple Pencil을 지원하기 위해 무엇이 필요한지 정확히 배우게 될 것입니다. 다음은 앞으로 배우게 될 핵심 사항입니다.

  • Apple Pencil의 강도를 조절하는 방법
  • 정확도를 높이는 방법
  • 음영 동작을 구현하는 방법
  • 지우개를 추가하는 방법
  • 예측 및 실제 drawing으로 작업하여 레이턴시를 개선하는 방법

이 튜토리얼이 끝나면 Apple Pencil 지원을 앱에 통합할 준비가 된 것입니다!

 

전제 조건

이 튜토리얼을 따라 하려면 다음이 필요합니다.

  • 아이패드(Apple Pencil 호환 모델)와 애플 펜슬(Apple Pencil, 이하 펜슬). 시뮬레이터에서 펜슬을 테스트할 수 없습니다.
  • Core Graphics에 대한 기본적인 지식. 컨텍스트가 무엇인지, 컨텍스트를 만드는 방법 및 획을 그리는 방법을 알아야 합니다. Core Graphics 튜토리얼의 첫 번째 부분을 살펴보세요. 속도를 높이는 데 충분할 것이며 앱은 물을 마시도록 상기시켜줍니다. :]

 

시작하기

이 Apple Pencil 튜토리얼 전체에서 Scribble이라는 앱을 구축하게 됩니다. 반응형 UI(예를 들면 압력 감도 및 음영 등)로 그림을 그릴 수 있는 간단한 앱입니다.

Scribble을 다운로드(구글 드라이브)하고 탐색해보세요. iPad Pro에서 연필과 손가락을 사용하여 그림을 그릴 때 화면에 손을 대고 사용해 보세요.

 

 

iPad를 흔들어 화면을 지우세요!

내부적으로 Scribble은 손가락이나 연필의 터치를 캡처하는 캔버스 뷰로 구성된 기본 앱입니다. 또한 터치를 반영하여 디스플레이를 지속적으로 업데이트합니다.

CanvasView.swift의 코드를 살펴보세요.

가장 중요한 코드는 사용자가 캔버스 보기와 상호 작용할 때 트리거되는 touchesMoved(_:with:)에서 찾을 수 있습니다. 이 메서드는 Core Graphics 컨텍스트를 만들고 캔버스 보기에 의해 표시되는 이미지를 해당 컨텍스트로 그립니다.

touchMoved(_:with:) 는 다음 drawStroke(_:touch:) 를 호출하여 이전 터치와 현재 터치 사이의 그래픽 컨텍스트에 선을 그립니다.

touchMoved(_:with:) 는 캔버스 보기에 표시된 이미지를 그래픽 컨텍스트에서 업데이트된 이미지로 바꿉니다.

아주 간단합니다. :]

 

Apple Pencil로 그리는 첫 그림

이제 펜슬의 첫 번째 기능인 힘을 사용할 준비가 되었습니다. 화면을 더 세게 누르면 획(스트로크)이 더 넓어집니다. 나중에 배우게 될 약간의 치트가 있지만 이 기능은 손가락으로 작동하지 않습니다.

힘의 양은 touch.force에 기록됩니다. 1.0의 힘은 평균 터치의 힘이므로 올바른 획 너비를 생성하려면 이 힘에 무언가를 곱해야 합니다. 잠시 후에 더 자세히…

CanvasView.swift를 열고 클래스 맨 위에 다음 상수를 추가합니다.

private let forceSensitivity: CGFloat = 4.0

 

forceSensitivity 상수를 조정하여 스트로크 너비를 압력에 다소 민감하게 만들 수 있습니다.

lineWidthForDrawing(_:touch:)을 찾습니다. 이 방법은 선의 너비를 계산합니다.

return 문 바로 앞에 다음을 추가합니다.

if touch.force > 0 {
  lineWidth = touch.force * forceSensitivity
}

 

터치의 힘(force)과 forceSensitivity 승수를 곱하여 선 너비를 계산합니다. 다만 이는 손가락이 아닌 연필에만 적용된다는 점을 기억하세요. 손가락을 사용하는 경우 touch.force0이 되므로 획 너비를 변경할 수 없습니다.

빌드 및 실행합니다. 펜슬로 몇 개의 선을 그리고 화면을 얼마나 세게 누르느냐에 따라 획이 어떻게 달라지는지 확인합니다.

 

더 부드럽게 그리기

그릴 때 선에 자연스러운 곡선이 아닌 날카로운 점이 있음을 알 수 있습니다. 펜슬을 사용하기 전에는 그림을 보기 좋게 만들기 위해 획을 스플라인 곡선(spline curves)으로 변환하는 것과 같은 복잡한 작업을 수행해야 했지만 펜슬을 사용하면 이러한 종류의 해결 방법이 크게 필요하지 않습니다.

애플은 아이패드 프로가 초당 120번 터치를 스캔하지만, 펜슬이 화면 가까이에 있을 때 스캔 속도는 초당 240번으로 두 배라고 말합니다.

iPad의 디스플레이 주사율 60Hz(초당 60회)입니다. 이것은 120Hz의 스캔으로 이론적으로 두 개의 터치를 인식할 수 있지만 하나만 표시할 수 있음을 의미합니다. 또한, 백그라운드에서 처리가 많을 경우 메인 스레드가 점유되어 처리할 수 없기 때문에 특정 프레임에서 터치 이벤트를 모두 놓칠 수 있습니다.
(참고로 최신 iPad Pro 모델은 디스플레이 120Hz 주사율을 지원합니다.)

빠르게 원을 그려보세요. 원형이어야 하지만 결과는 다각형의 들쭉날쭉한 점과 더 유사합니다.

 

Apple은 이 문제를 해결하기 위해 통합된 터치(coalesced touches)라는 개념을 제안했습니다. 기본적으로 새 UIEvent 배열에서 손실된 터치를 캡처하며, 이는 UIEvent.coalescedTouches(for:)를 통해 액세스할 수 있습니다.

CanvasView.swift에서 touchMoved(_:with:) 안에서 아래의 코드를 찾은 뒤 그 다음 코드로 치환합니다.

drawStroke(context, touch: touch)
// 1
var touches = [UITouch]()
    
// 2
if let coalescedTouches = event?.coalescedTouches(for: touch) {
  touches = coalescedTouches
} else {
  touches.append(touch)
}
    
// 3
print(touches.count)

// 4
for touch in touches {
    drawStroke(context, touch: touch)
}

 

섹션별로 살펴보겠습니다.

  1. 먼저 처리해야 할 모든 터치를 저장할 새 배열을 설정합니다.
  2. 병합된 터치가 있는지 확인하고 거기에 있으면 모두 새 어레이에 저장합니다. 없으면 기존 배열에 원터치로 추가하면 됩니다.
  3. 처리 중인 터치 수를 보려면 로그 문을 추가하세요.
  4. 마지막으로 drawStroke(_:touch:)를 한 번만 호출하는 대신 새 배열에 저장된 모든 터치에 대해 호출합니다

빌드 및 실행합니다. 펜슬로 멋진 소용돌이 곡선(curlicues)을 그리고, 버터 같은 부드러움과 획 너비의 제어를 즐기세요.

 

 

디버그 콘솔에 주의를 기울이세요. 손가락이 아닌 펜슬로 그릴 때 더 많은 터치 입력을 받는다는 것을 알 수 있습니다.

또한 터치를 합친 경우에도 iPad Pro가 펜슬을 감지할 때 터치를 두 배 더 스캔하기 때문에 펜슬로 그린 원이 훨씬 더 둥글다는 것을 알 수 있습니다.

 

펜슬 기울이기

이제 앱에 멋진 유창한 그림이 생겼습니다. 그러나 Apple Pencil에 대한 리뷰를 읽거나 본 적이 있다면 연필과 같은 음영 기능에 대한 이야기가 있다는 것을 기억할 것입니다. 사용자는 기울이기만 하면 되지만 음영이 자동적으로 발생하지 않는다는 사실은 거의 깨닫지 못합니다. 예상대로 작동하도록 코드를 작성하는 것은 모두 똑똑한 앱 개발자에게 달려 있습니다. :]

 

고도, 방위각 및 단위 벡터 (Altitude, Azimuth and Unit Vectors)

이 섹션에서는 기울기를 측정하는 방법을 설명합니다. 다음 섹션에서 간단한 음영 처리에 대한 지원을 추가하게 될 것입니다.

연필로 작업할 때 3차원으로 회전할 수 있습니다. 위아래 방향을 고도(altitude)라고 하고 좌우 방향을 방위각(azimuth)이라고 합니다.

 

UITouchaltitudeAngle 속성은 iOS 9.1의 새로운 기능이며 Apple Pencil에만 있습니다. 라디언(radian)으로 측정한 각도입니다. 펜슬이 iPad 표면에 평평하게 놓여 있을 때의 고도는 0입니다. 화면의 점과 똑바로 서 있을 때 고도는 π/2입니다. 360도 원은 라디언이므로 π/2는 90도와 같습니다.

방위각을 가져오는 UITouch에는 azimuthAngle(in:)azimuthUnitVector(in:)의 두 가지 메서드가 있습니다. 가장 저비용인 것은 azimuthUnitVector(in:)이지만 둘 다 유용합니다. 상황에 가장 적합한 것은 계산할 항목에 따라 다릅니다.

방위각의 단위 벡터가 작동하는 방식을 살펴봅니다. 참고로 단위 벡터는 길이가 1이고 좌표(0,0)에서 방향을 향하는 점을 가집니다.

 

직접 확인하려면 guard 문 바로 뒤에 있는 touchMoved(_:with:) 상단에 다음을 추가하세요.

print(touch.azimuthUnitVector(in: self))

 

빌드 및 실행합니다. iPad가 가로 방향으로 놓고 펜촉이 화면 왼쪽에 닿고 끝이 오른쪽으로 기울어지도록 펜슬을 잡습니다.

디버그 콘솔에서 이러한 값을 만족스러운 정밀도로 얻을 수는 없지만, 벡터는 x 방향으로 약 1 단위이고 y 방향으로 0 단위, 즉 (1, 0)입니다.

펜슬의 끝이 iPad 바닥을 가리키도록 시계 반대 방향으로 90도 회전합니다. 그 방향은 대략 (0, -1)입니다.

x 방향은 코사인을 사용하고 y 방향은 사인을 사용합니다. 예를 들어, 위 그림과 같이 펜을 잡고 있다면(원래 수평 방향에서 반시계 방향으로 약 45도) 단위 벡터는 (cos(45), sin(-45)) 또는 (0.7071, -0.7071)입니다.

 

참고: 벡터에 대해 잘 모르는 경우, Sprite Kit를 사용하는 게임용 삼각법에 대한 튜토리얼이 있습니다.

 

펜슬의 방향을 변경하면 연필이 가리키는 위치를 나타내는 벡터가 어떻게 제공되는지 이해했다면면 print문을 제거하세요.

 

음영으로 그리기

기울기를 측정하는 방법을 알았으므로 이제 Scribble에 간단한 음영을 추가할 준비가 되었습니다.

펜슬이 자연스러운 드로잉 각도에 있을 때 힘을 사용하여 선을 그려서 굵기를 결정하지만 사용자가 펜슬을 옆으로 기울이면 힘을 사용하여 음영의 불투명도를 측정합니다.

또한 획의 방향과 연필을 잡고 있는 방향에 따라 선의 굵기를 계산합니다.

여기에서 이해가 되지 않는다면, (실제) 연필과 종이를 찾아 연필을 옆으로 돌려서 연필심이 종이와 최대한 접촉하도록 하여 음영을 시도하세요. 연필을 기울이는 방향과 같은 방향으로 음영을 넣으면 음영이 얇아집니다. 그러나 연필에 90도 각도로 음영을 줄 때 음영이 가장 두꺼워집니다.

 

텍스처 작업

첫 번째 순서는 실제 연필로 음영 처리 된 것처럼 보이도록 선의 질감을 변경하는 것입니다. 시작 앱에는 이를 위해 사용할 PencilTexture라는 Assets 카탈로그의 이미지가 포함되어 있습니다.

CanvasView 상단에 이 속성을 추가합니다.

private var pencilTexture = UIColor(patternImage: UIImage(named: "PencilTexture")!)

 

이렇게 하면 지금까지 사용한 기본 빨간색 대신 pencilTexture를 사용할 색상으로 지정할 수 있습니다.

drawStroke(_:touch:)에서 다음 라인을 찾은 뒤 변경합니다..

drawColor.setStroke()

// 를 다음으로 변경

pencilTexture.setStroke()

 

빌드 및 실행하면 이제 마법같이 선이 실제 연필 선처럼 보입니다.

 

이 튜토리얼에서는 다소 나이브한 방식으로 연필 텍스처를 사용하고 있습니다. 모든 기능을 갖춘 아트 앱의 브러시 엔진은 훨씬 더 복잡하지만 시작 단계에서는 이 접근 방식으로 충분합니다.

 

펜슬이 음영을 표현할 만큼 충분히 기울어져 있는지 확인하려면 CanvasView 상단에 다음 상수를 추가하세요.

private let tiltThreshold = π/6  // 30º

이 값을 다르게 잡고 있기 때문에 작동하지 않는 경우 해당 값을 적절하게 변경할 수 있습니다.

 

π를 입력하려면 영문 자판 상태에서 Option + P를 동시에 누릅니다. πCanvasView.swift 상단에 CGFloat(Double.pi)로 정의된 편의상수입니다.

그래픽을 프로그래밍할 때 도(degree) 단위로 다시 변환하는 것보다 라디언 단위로 생각하는 것이 중요합니다. 라디언과 도 사이의 상관 관계를 보려면 Wikipedia에서 이 이미지를 살펴보세요.

 

 

그런 다음 drawStroke(_:touch:)에서 다음 줄을 찾습니다.

let lineWidth = lineWidthForDrawing(context, touch: touch)

 

이것을 다음으로 변경합니다.

var lineWidth: CGFloat

if touch.altitudeAngle < tiltThreshold {
  lineWidth = lineWidthForShading(context, touch: touch)
} else {
  lineWidth = lineWidthForDrawing(context, touch: touch)
}

 

여기에 당신의 연필이 π/6 또는 30도 이상 기울어져 있는지 확인하기 위한 체크 과정을 추가하고 있습니다. true인 경우 그리기 대신 음영을 호출합니다.

이제 CanvasView의 맨 아래에 이 메서드를 추가합니다.

private func lineWidthForShading(_ context: CGContext?, touch: UITouch) -> CGFloat {
    
    // 1
    let previousLocation = touch.previousLocation(in: self)
    let location = touch.location(in: self)
    
    // 2 - vector1 is the pencil direction
    let vector1 = touch.azimuthUnitVector(in: self)
    
    // 3 - vector2 is the stroke direction
    let vector2 = CGPoint(x: location.x - previousLocation.x, y: location.y - previousLocation.y)
    
    // 4 - Angle difference between the two vectors
    var angle = abs(atan2(vector2.y, vector2.x) - atan2(vector1.dy, vector1.dx))
    
    // 5
    if angle > π {
        angle = 2 * π - angle
    }
    if angle > π / 2 {
        angle = π - angle
    }
    
    // 6
    let minAngle: CGFloat = 0
    let maxAngle = π / 2
    let normalizedAngle = (angle - minAngle) / (maxAngle - minAngle)
    
    // 7
    let maxLineWidth: CGFloat = 60
    var lineWidth = maxLineWidth * normalizedAngle
    
    return lineWidth
}

여기에는 몇 가지 복잡한 수학이 있으므로 단계별로 설명합니다.

  1. 이전 터치 포인트와 현재 터치 포인트를 저장합니다.
  2. 연필의 방위각 벡터를 저장합니다.
  3. 그리는 획의 방향 벡터를 저장합니다.
  4. 획 선과 연필 방향 사이의 각도 차이를 계산합니다.
  5. 각도를 줄여 0~90도가 되도록 합니다. 각도가 90도이면 스트로크가 가장 넓습니다. 모든 계산은 라디언으로 이루어지며 π/2는 90도임을 기억하세요.
  6. 이 각도를 0과 1 사이에서 정규화(normalization)합니다. 여기서 1은 90도입니다.
  7. 올바른 음영 너비를 얻으려면 최대 선 너비 60을 정규화된 각도로 곱하세요.

 

참고: 펜슬로 작업할 때마다 다음 공식이 유용합니다.

벡터의 각도: angle = atan2(opposite(반대), adjacent(인접))
정규화: normal = (값 - minValue) / (maxValue - minValue)

 

빌드 및 실행합니다. 그림에 표시된 각도로 연필을 잡으세요. 각도를 변경하지 않고 약간의 음영을 만들 수 있습니다.

획 방향이 변경됨에 따라 어떻게 더 넓어지고 좁아지는지 확인하세요. 이 나이브한 접근 방식으로는 약간 지저분하지만, 잠재적인 면을 확실히 볼 수 있습니다.

 

방위각(azimuth)을 사용하여 폭 조정

실제 연필로 90도로 그리면 연필의 기울기 각도를 변경함에 따라 선이 좁아집니다. 그러나 Apple Pencil로 시도하면 선 너비가 동일하게 유지됩니다.

방위각 외에도 선의 너비를 계산할 때 펜슬의 고도(altitude)도 고려해야 합니다.

CanvasView 클래스의 상단 멤버 변수 부분에 다음을 추가하세요.

private let minLineWidth: CGFloat = 5

 

이것은 음영 선의 가장 좁은 영역을 설정합니다. 자신의 개인 음영 취향에 맞게 변경할 수 있습니다. :]

lineWidthForShading(_:touch:) 하단의 return 문 바로 직전에 다음을 추가합니다.

// 1
let minAltitudeAngle: CGFloat = 0.25
let maxAltitudeAngle = tiltThreshold
    
// 2
let altitudeAngle = touch.altitudeAngle < minAltitudeAngle ? minAltitudeAngle : touch.altitudeAngle
    
// 3
let normalizedAltitude = 1 - ((altitudeAngle - minAltitudeAngle) / (maxAltitudeAngle - minAltitudeAngle))

// 4
lineWidth = lineWidth * normalizedAltitude + minLineWidth

이 코드를 lineWidthForDrawing(_:touch:)가 아닌 실수로 lineWidthForShading(_:touch:)에 추가했는지 확인하세요.

 

여기에서 소화해야 할 내용이 많기 때문에 조금씩 살펴보겠습니다.

  1. 이론적으로 펜슬의 최소 고도는 0도입니다. 즉, iPad에 평평하게 놓여 있고 팁이 화면에 닿지 않으므로 고도를 기록할 수 없습니다. 실제 최소 고도는 약 0.2이지만 위의 코드에서 최소 고도는 0.25로 설정했습니다.
  2. 고도가 최소값보다 낮으면, 최소값을 사용합니다.
  3. 이전과 마찬가지로 이 고도 값을 01 사이로 정규화합니다.
  4. 마지막으로 방위각으로 계산한 선 너비에 이 정규화된 값을 곱하고 이를 최소 선 너비에 더합니다.

 

빌드 및 실행합니다. 음영을 줄 때 펜슬의 고도를 변경하고 획이 어떻게 넓어지고 좁아지는지 확인하세요. 펜슬의 고도를 점차적으로 높이면 드로잉 라인에 부드럽게 들어갈 수 있습니다.

 

 

불투명도 표현

이 섹션의 마지막 작업은 힘으로 계산할 텍스처의 불투명도를 낮추어 음영을 좀 더 사실적으로 보이게 하는 것입니다.

lineWidthForShading(_:touch:)return 문 바로 직전에 다음을 추가합니다.

let minForce: CGFloat = 0.0 
let maxForce: CGFloat = 5 
let normalizedAlpha = (touch.force - minForce) / (maxForce - minForce) 
context?.setAlpha(normalizedAlpha)

 

이전 코드 블록을 살펴보면 이 코드는 자명합니다. 힘을 가하고 0과 1 사이의 값으로 정규화한 다음 컨텍스트에서 사용하는 알파를 해당 값으로 설정하기만 하면 됩니다.

빌드 및 합니다. 다양한 압력으로 음영 처리를 해보세요.

 

(하편에서 계속됩니다.)

문의 | 코멘트 또는 yoonbumtae@gmail.com


카테고리: Swift


0개의 댓글

답글 남기기

Avatar placeholder

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다