1 of 61

Unity Shader 공부회 (11/08)

정점 쉐이더 애니메이션 입문 [번역 금별]

2 of 61

자기소개

주식회사 블래스트엣지게임즈 소속

mao ( @TEST_H_ )

・ 게임 프로그래머

3 of 61

▼ 이번에 강의할 내용

「정점 쉐이더에서 오브젝트를 움직이려면 어떻게 하는가?」에 대하여 간단하게 해설한다.

→ 뒤에는 더 필요한 지식의 보충/일부 응용 예시 등을 다룰 예정이다.

・ 최근에는 그렇게 의식하지 않아도 되나, 알아두면 작업에 쫓길 때 어디선가 도움이 될 지도 모른다.

전체적으로 초심자용의 내용을 다룬다

※그러므로 특별히 이거다 하는 응용 테크닉 설명 등은 없으므로 어느정도 알고 계신 분들에게는 지루할 수도 있다.

4 of 61

▼ 이번에 강의하지 않는 내용에 대해

・렌더링 파이프라인 전체

・ShaderLab의 언어 사양 등- ※ 추천서적은 후술함

・정점 쉐이더 이외의 스테이지

- 플러그멘트 쉐이더 등� - ※ 지오메트리 쉐이더 등은 시간이 있다면 조금 다룰 수도 있다.

5 of 61

※ ShaderLab의 언어 사양에 대해

「Unity쉐이더 프로그래밍의 교과서 ShaderLab언어해설편」

이것을 추천

※초심자용 내용이라고 말하면서 갑자기 언급해서 죄송합니다...

https://s-games.booth.pm/items/660001(일어)

언어 사양의 파악 이외에도 첫장에�렌더링 파이프라인/각종 좌표 공간 등을 가볍게 다루고 있다.� 

6 of 61

▼ 그 외

Unlit Shader를 전제로 해설

・ 구현한 샘플은 GitHub에서 공개

- mao-test-h/VertexShaderSample(일어)

・ 자료는 가능한 빨리 공개할 예정

7 of 61

▼ 목차

 ▽ 정점 쉐이더란?

 ▽ 정점을 움직이는 법

 ▽ 부록1. 애니메이션 예(※)

 ▽ 부록2. 지오메트리 쉐이더를 가볍게 다룸

8 of 61

정점 쉐이더란?

9 of 61

왼쪽의 코드는 심플한�Unlit Shader

정점 좌표(모델 공간)

텍스쳐 UV

정점 좌표(클립 공간)

텍스쳐 UV

정점 쉐이더

MVP 행렬

플래그먼트 쉐이더

10 of 61

왼쪽의 코드는 심플한�Unlit Shader

이번에 다룰 내용과 연관있는 곳은 빨간 선으로 표시하였다

정점 좌표(모델 공간)

텍스쳐 UV

정점 좌표(클립 공간)

텍스쳐 UV

정점 쉐이더

MVP 행렬

플래그먼트 쉐이더

11 of 61

▼ appdata?

・입력된 정점 정보

→ 렌더링된 지오메트리에 관한 정보

- 정점 좌표(모델공간)

- 텍스쳐UV

정점 좌표(모델 공간)

텍스쳐 UV

12 of 61

▼ vert

정점 쉐이더

입력된 appdata를 좌표변환 하여,�v2f(vertex to fragment)에 넣어 반환한다.

→ v2f는 플러그멘트 쉐이더를 사용

※ v2f (vertex to fragment)

정점 쉐이더

MVP 행렬

정점 좌표(클립 공간)

텍스쳐 UV

13 of 61

▼ vert

MVP행렬이란?

→ 모델 x 뷰 x 프로젝션 행렬

정점 쉐이더

MVP 행렬

14 of 61

모델 x 뷰 x 프로젝션행렬

정점 쉐이더의 역할로서 카메라가 비추는 정보를 2차원(디스플레이상)의 어느 위치에 표시할 지를 정할 필요가 있다.

→ 그것을 위한 좌표변환을 사용

모델변환 - 입력된 정점을 로컬 공간좌표로부터 월드 공간좌표로 변환

뷰 변환 - 월드 공간좌표로부터 카메라 공간좌표로 변환

프로젝션 변환

- 카메라 공간좌표 → 클립 공간좌표로 변환

15 of 61

모델 x 뷰 x 프로젝션행렬

정점 쉐이더의 움직임을 파악하기 위해서는 의외로 중요한 점이지만, 본제(애니메이션)과 조금 벗어날 수 있기에 조금 만 언급하겠다.

※참고사이트 기재(일어)

opengl-tutorial : 튜토리얼 3 행렬

버틱 쉐이더에 의한 좌표계변환

MVP행렬에 의한 좌표변환에 대해

16 of 61

▼ vert

MVP행렬이란?

→ 모델 x 뷰 x 프로젝션 행렬��본론으로 돌아와�이번에 강의할 내용은 위에 기재한�좌표변환을 할 때 로컬좌표의 정점을 움직임으로서 애니메이션의 처리를 행한다에 관해�해설할 것이다

← ※이 값을 변경

정점 쉐이더

MVP 행렬

17 of 61

정점을 움직이는 법

18 of 61

▼ 간단한 예시

입력된 정점의 로컬좌표에 이동치를 가산하는 것 뿐인 예시

※”_Position”은 Properties에서 선언하고있다

※참고로 「v.vertex.xyz」라고 하는� 스위즐(swizzle)연산자를 이용한 기법도 있다

정점에 이동치를 더하는 것 뿐

MVP 변환

정점 좌표(모델 공간)

텍스쳐 UV

19 of 61

▼ 간단한 예시

스위즐(swizzle)연산자란?

벡터와 행렬의 각 요소에 접근하기 위한 기법�→ 옆의 코드는 벡터의 예

예로서, 이하의 예1과 예2는 같은 결과를 불러온다

float3 _Position;

// 예1

float2 ret = float2(_Position.x, _Position.y);

// 예2

float2 ret = _Position.xy;

※ 여담

Unity.Mathematicsfloat4 등의 형태는 대응하고 있다.�(다만 EditorBrowsableState.Never로 감춰져있다)

정점에 이동치를 더하는 것 뿐

MVP 변환

20 of 61

▼ 간단한 예시

예로서 ECS의 샘플에 있는�BoidExample의 물고기는�이 방법으로 꾸불꾸불하게 움직인다

Unity-Technologies/EntityComponentSystemSamples

Wiggle.Shader

꾸불꾸불

21 of 61

▼ 정점을 움직이는 법

앞서 다룬 간단한 예시를 통해 움직이는 법은 가능하다.

→ 이와 더불어 정점 쉐이더에서 오브젝트를 확대하거나 회전시키고 싶을 때에는 어떻게 하는가?

22 of 61

▼ 정점을 움직이는 법

앞서 다룬 간단한 예시를 통해 움직이는 법은 가능하다.

→ 이와 더불어 정점 쉐이더에서 오브젝트를 확대하거나 회전시키고 싶을 때에는 어떻게 하는가?

이는, 변환 행렬에서 쉽게 구현할 수있다.

23 of 61

▼ 변환행렬에 대해

변환행렬이란?

→ 검색하면 다양하게 나올 것이다.

・모델 변환행렬

- 평행이동행렬, 확대축소행렬, 회전행렬

・뷰 변환행렬

・프로젝션 변환행렬

24 of 61

▼ 변환행렬에 대해

변환행렬이란?

→ 검색하면 다양하게 나올 것이다.

・모델 변환행렬 ☆이번에 다룰 것

- 평행이동행렬, 확대축소행렬, 회전행렬

・뷰 변환행렬

・프로젝션 변환행렬

25 of 61

▼ 변환행렬에 대해

무엇을 할 수 있는가?�어떻게 적용하는가?

→ 대략적으로 말하자면...

 「행렬」과「정점」을 곱하는 일이 가능함(좌표를 변환할 수 있다.)

※다음 페이지에서 오브젝트를 90도 회전시키는 예를 기재

26 of 61

※ X축으로 90도 회전시키는 예시

▼ 적용전(회전행렬을 코멘트아웃한 상태)

▼ 적용후 ※오른쪽 코드대로

※회전행렬에 대해서는 다음에 별도 해설

X축회전

MVP 변환

27 of 61

각종 변환행렬은 정점의 좌표를 움직이기 위한 필터라고 쉽게 말할 수 있다.

옆에 나타낸 예시로 살펴보자면 오브젝트 자체는 평면 폴리곤이기때문에 정점의 수는 전부 4개이다.

그렇기에 정점 쉐이더에는 4 정점이 appdata로서 입력되므로, 모든 입력(정점)에 대해

같은 회전행렬을 곱함으로써 오브젝트 자체가 90도 회전하는 결과가 나타난다

X축에 90도 돌리는 회전행렬

mul(rotMatX, v.vertex)

28 of 61

▼ 평면이동행렬

※ ”_Position”은 Properties에서 정의.

  이후의 처리도 이와 같다.

평면이동

MVP 변환

29 of 61

▼ 확대축소행렬

스케일

MVP 변환

30 of 61

▼ X축 회전행렬

회전관련은 Transform의 Inspector에 맞추기 위해

MVP 변환

x축회전

일부러 degree에서 radian에 변환하고 있다

31 of 61

▼ Y축 회전행렬

회전관련은 Transform의 Inspector에 맞추기 위해

MVP 변환

Y축회전

일부러 degree에서 radian에 변환하고 있다

32 of 61

▼ Z축 회전행렬

※ 회전행렬을 쭉 설명하였으나,

Quaternion를 회전행렬으로 변환하여 사용하는 방법도 있다.

회전관련은 Transform의 Inspector에 맞추기 위해

MVP 변환

Z축회전

일부러 degree에서 radian에 변환하고 있다

33 of 61

「축소 → 회전 → 이동」의 순서로 진행하는 것으로 합치는 것도 가능하다.�

→순서가 중요하다.

스케일

X축회전

Y축회전

Z축회전

평행이동

34 of 61

「축소 → 회전 → 이동」의 순서로 진행하는 것으로 합치는 것도 가능하다.�

→순서가 중요하다.

※ 주의점 ~행우선과 열우선에 관하여~

Unity는 Script(C#)측은 열우선으로 되어있으나�Shader쪽은 행우선으로 되어있는 상태�→ 예를 들어 컨스트럭터(생성자)에서 행렬을 초기화할 때의 인수의 순서는 행 우선을 준수.

다만, 계산자체는 열 백터를 상정하여 작업할 필요가 있다.�→그것을 위해 「행렬 x 벡터」를 곱하고 있다.

참고 : 공간과 플랫폼의 사이에서 �   – Unity의 좌표변환에 연관된 이야기(일어)–

스케일

X축회전

Y축회전

Z축회전

평행이동

35 of 61

「축소 → 회전 → 이동」의 순서로 진행하는 것으로 합치는 것도 가능하다.�

→순서가 중요하다.

※ 여담

주로 ECS & JobSystem쪽에서�사용하는 「Unity.Mathematics」�라는 라이브러리가 있으나이쪽은 지금으로서는 열우선을 상정하나, 옛날 버전에는 행우선인 것들도 있다.→ 쉐이더와는 관계 없는 내용이지만 옛날 샘플을 사용하거나 할 때에는 주의할 것.

(※사고를 shader언어로 맞추기 위해??)

스케일

X축회전

Y축회전

Z축회전

평행이동

36 of 61

▼ 보충

참고로, 옆의 예시처럼 쓰는 것도 가능하다.

스케일

X축회전

평행이동

모델 변환행렬을 취득(unity_ObejectToWorld는 Unity에서 참조할 수 있는 행렬)

이것으로 조절하는 것도 가능하나

직접 대입하면 다른 변환(회전)과 충돌을 일으킬 수도 있다

float4x4를 사용하지 않고도 스위즐 연산자를 이용하여 이렇게 쓸 수 있다

평행이동은 다른 변환의 영향을 받지 않기 때문에 직접 대입해도 문제 없다

다만, Transform.position의 값이 반영되지 않으며 카메라 밖으로 이동시키면 컬링된다.

모델변환

VP변환

37 of 61

▼ 보충

「unity_ObjectToWorld」은 모델변환행렬

옆의 예시는 오브젝트가 가지는 변환행렬의 값을 직접 텍스트 수정하는 것으로 움직이거나, 변형시키고 있다

평행이동은 다른 변환의 영향을 받지 않으므로 직접 대입하는 것으로 움직이는 것도 가능하다.

→ 다만 이 방법에는 주의점할 점이 있다.

- transform.position의 값을 덮어쓰는 것과 같은 것으로, 이 쪽에 값을 넣어도 움직이지 않는다.(움직이지 않는것처럼 보인다)

- 거기다 때에 따라서는 컬링이 유효해지므로�transform 경유로 카메라 밖으로 이동시키면 사라지거나.

스케일

모델 변환행렬을 취득(unity_ObejectToWorld는 Unity에서 참조할 수 있는 행렬)

이것으로 조절하는 것도 가능하나

직접 대입하면 다른 변환(회전)과 충돌을 일으킬 수도 있다

X축회전

float4x4를 사용하지 않고도 스위즐 연산자를 이용하여 이렇게 쓸 수 있다

평행이동

평행이동은 다른 변환의 영향을 받지 않기 때문에 직접 대입해도 문제 없다

다만, Transform.position의 값이 반영되지 않으며 카메라 밖으로 이동시키면 컬링된다.

모델변환

VP변환

38 of 61

▼ 보충

스케일

모델 변환행렬을 취득(unity_ObejectToWorld는 Unity에서 참조할 수 있는 행렬)

이것으로 조절하는 것도 가능하나

직접 대입하면 다른 변환(회전)과 충돌을 일으킬 수도 있다

X축회전

float4x4를 사용하지 않고도 스위즐 연산자를 이용하여 이렇게 쓸 수 있다

평행이동

평행이동은 다른 변환의 영향을 받지 않기 때문에 직접 대입해도 문제 없다

다만, Transform.position의 값이 반영되지 않으며 카메라 밖으로 이동시키면 컬링된다.

모델변환

VP변환

「unity_ObjectToWorld」은 모델변환행렬

옆의 예시는 오브젝트가 가지는 변환행렬의 값을 직접 텍스트 수정하는 것으로 움직이거나, 변형시키고 있다

평행이동은 다른 변환의 영향을 받지 않으므로 직접 대입하는 것으로 움직이는 것도 가능하다.

→ 다만 이 방법에는 주의점할 점이 있다.

- 예시로서 소개하고 있으나,

충분히 이해하지 않고 조작하면 사고날 가능성도 있다.

39 of 61

▼ 보충

덧붙여 ”matrix._11_22_33”라고 기재한 것은�행렬 스위즐 연산자

→ “_[행][렬]”에서 요소를 지정하여 엑세스 할 수 있다

※ 기법으로는 0에서 시작하는 기법이라든지, 1에서 시작하는 기법이라든지 있는 것 같다

・ 0스타트 : _m00, _m01, _m02, _m03

・ 1스타트 : _11, _12, _13, _14

참고 : 성분 자체로 연산 (DirectX HLSL)(일어)

스케일

모델 변환행렬을 취득(unity_ObejectToWorld는 Unity에서 참조할 수 있는 행렬)

이것으로 조절하는 것도 가능하나

직접 대입하면 다른 변환(회전)과 충돌을 일으킬 수도 있다

X축회전

float4x4를 사용하지 않고도 스위즐 연산자를 이용하여 이렇게 쓸 수 있다

평행이동

평행이동은 다른 변환의 영향을 받지 않기 때문에 직접 대입해도 문제 없다

다만, Transform.position의 값이 반영되지 않으며 카메라 밖으로 이동시키면 컬링된다.

모델변환

VP변환

40 of 61

▼ 보충

앞서 설명한 부분에서는�float4x4를 전제로 해설하였으나�용도에 따라 옆의 예시처럼�필요한 사이즈(예를 들어 X축회전이면서 float2x2)를 가지고 작업하는 것도 가능하다.

→ 반드시 4x4로 작업할 필요는 없다.

스케일

모델 변환행렬을 취득(unity_ObejectToWorld는 Unity에서 참조할 수 있는 행렬)

이것으로 조절하는 것도 가능하나

직접 대입하면 다른 변환(회전)과 충돌을 일으킬 수도 있다

X축회전

float4x4를 사용하지 않고도 스위즐 연산자를 이용하여 이렇게 쓸 수 있다

평행이동

평행이동은 다른 변환의 영향을 받지 않기 때문에 직접 대입해도 문제 없다

다만, Transform.position의 값이 반영되지 않으며 카메라 밖으로 이동시키면 컬링된다.

모델변환

VP변환

41 of 61

▼ 응용예시

정점의 위치와 법선의 위치를

Texture에 써서

키 프레임에 대응하는 위치를 참고하여 정점을 움직이는 애니메이션을 만드는 예

→ appdata의 정보는 사용하지 않았다.

키 프레임의 취득

정점의 위치와 법선을 취득

좌표변환

이 예시라면 모델 변환은 C#쪽에서 할 수 있으므로, vert쪽에서는 view, projection만으로 좋다.

_PlayDataBuffer[instanceID].LocaltoWorld 에서 모델변환행렬을 참조

42 of 61

▼ 응용예시

←옆의 기능에 대해서는 이하의 프로젝트를 사용하였다

sugi-cho/Animation-Texture-Baker(일어)

※ 표시한 소스 자체는

이전에 제가 Animation-Texture-Baker를 가지고 ECS에서 움직이는 애니메이션 시스템을 만들었을 때 자작한 것입니다

→그렇기에 keyframe등을 넘길 수 있도록 되어 있어요

mao-test-h/ECS-AnimationSample(일어)

키 프레임의 취득

정점의 위치와 법선을 취득

좌표변환

이 예시라면 모델 변환은 C#쪽에서 할 수 있으므로, vert쪽에서는 view, projection만으로 좋다.

_PlayDataBuffer[instanceID].LocaltoWorld 에서 모델변환행렬을 참조

43 of 61

초심자편?

END

44 of 61

부록1. 애니메이션 예시

※무엇을 움직이는지는 다음 페이지에서

45 of 61

46 of 61

▼ 저 로고를 회전시켜보자

어떻게 회전시켰는가?

→ 포인트는 2가지

・프레임이 끊기는 애니메이션 실현

・어떻게 원점을 하단에 설정시켜 회전시켰는가?

47 of 61

▼ 프레임이 끊기는 애니메이션에 대해

프레임이 끊기는 표현에 대해서는

「0~(π/2)」까지의 각도를 테이블로 가지고

_SinTime에서 취득할 수 있는 값을 베이스로

index에서 변환하는 것을 회전값으로 하고 있다.�

프레임이 끊기는 애니메이션(radians)

sin에서 취득할 수 있는 -1~1의 값을 0~1의 범위에 정규화

SinTime의 값을 0~15의 범위에 스케일

값을 양자화하는 것으로 애니메이션 테이블의 Intdex로 취급

회전행렬

48 of 61

※ 이미지

49 of 61

조금 더 자세히 설명하자면

sin파에서 얻을 수 있는 값에 대해서는

-1~1의 범위가 되므로

그대로 Index로서

사용할 수는 없다.

50 of 61

그래서 이번에는 거기서

-1~1의 범위를 0~1의 값으로 변환하는 것으로 하였다.

> float normal = (_SinTime.w + 1) / 2;

51 of 61

그리고 이것의 정규화한 값에 대해

애니메이션 테이블의 길이를 걸고

또 round에서 양자화하는 것으로

「0~애니메이션의 길이」의 파동으로

변환하는 것으로

index를 사용할 수 있었다.

> float rot = Animation[round(normal*15)];

52 of 61

애니메이션 테이블에서 얻은 값에 X축의 회전행렬을 생성

원점을 하단에 설정하여 회전시키는 방법에 대해서는�「Y방향에 Y스케일을 반쯤 엇갈림」→「회전」→「엇갈린 위치를 원위치로 되돌림」하는 방법으로 구현하였다.

회전행렬

원점을 하단에 설정하기 위해 오프셋을 엇갈리게 하여 회전시킨다

Y스케일 반쯤의 오프셋을 위에 엇갈리게 하여 회전을 걸고, 원래의 위치로 되돌린다

53 of 61

※이번에 설명한 회전처리 자체는 어디까지나 하나의 예시

회전처리에 관해 말하자면

예를 들어 「임의점 주변의 회전이동」을 계산하는 것으로

위치를 엇갈리게 하지 않아도 돌리는 방법도 가능하다.

참고 : 임의점 주변의 회전이동(일어) ※링크는 2D로 하였을 때

회전행렬

원점을 하단에 설정하기 위해 오프셋을 엇갈리게 하여 회전시킨다

Y스케일 반쯤의 오프셋을 위에 엇갈리게 하여 회전을 걸고, 원래의 위치로 되돌린다

54 of 61

부록2. 지오메트리 쉐이더 맛보기

55 of 61

▼ 샘플 소개

지오메트리 쉐이더를 응용하여

모양 관계 없이 강제적으로 로고를 덧칠하는 쉐이더

→이것을 구현하는 포인트 해설

※ 지오메트리 쉐이더를 사용한 예시의 하나 정도로 받아들여주시면 감사하겠습니다..

56 of 61

▼구현에 대해

하는 일을 간단하게 정리하면

・ 정점 쉐이더에서 좌표변환 등을 하지 않고 입력된 UV나 VertexID를 지오메트리 쉐이더에 거는 것 뿐이다

・ 지오메트리 쉐이더에서는 1정점을 입력으로서 받아들이고, 내부에 평면 폴리곤(4정점)을 생성� →생성한 평면 폴리곤의 4정점에 대해, 아까 설명한 회전변환이나 MVP변환을 적용

57 of 61

▼ 구현에 대해

정점 쉐이더

geom쪽에 SV_VertexID를 넘기지 않는 대신 사용하지 않는 uv.z를 넣어둔다

v2g는 정점 쉐이더에서 지오메트리 쉐이더에 출력

g2f는 지오메트리 쉐이더에서 플래그먼트 쉐이더에 출력

정점 쉐이더쪽에서 좌표변환을 하지 않고도�입력으로 얻은 정점 정보를�v2g에 저장하여 내보내는 것 뿐

※ VertexID는 그대로 지오메트리 쉐이더에 전달되지 않는 것 같으므로 사용하지 않는 uv.z에 넣고 있다. 

58 of 61

지오메트리 쉐이더 쪽에서는 로고에 맞춘 정점 정보를 테이블로서 유지

→ Vertices[4], UVs[4]

입력을 받아들이면 테이블을 기반으로 정점을 생성하여�생성된 정점에 대해 원래대로의 회전처리의 산출, 적용, 좌표변환을 하는 것 뿐

→ 실제로 정점 쉐이더의 입력은 �  VertexID만 참고하고있다.

※옆의 코든 지오메트리 쉐이더의 코드에서 일부 발췌한 것(전체는 길어서 안들어가요;;)

지오메트리 쉐이더

인수에는 하나의 정점만 받아들인다

안에서 네개의 정점으로 늘리는 것으로 평면 폴리곤을 생성, Texture를 붙이는 느낌

1정점분만 로고를 생성(여기서 막지 않으면 정점분량만큼의 로고가 생성된다

로고 데이터(정점, UV)

회전행렬의 산출(할애)...

정점의 생성

원점을 하단에 설정하기 위해 오프셋을 엇갈리게 하여 회전시킨다

Y스케일의 반쯤의 오프셋을 엇갈리게 하여 회전을 걸고, 원래의 위치로 되돌린다

여기에 생성한 원점에 대한 좌표를 생성

59 of 61

참고로 앞쪽에서 VertexID를 보면 �움직이면 정점마다 불려나오기 때문에�로고가 정점분 생성되어버리므로 생성되는 갯수를 1개로 억제하고 있는 것 뿐

지오메트리 쉐이더

인수에는 하나의 정점만 받아들인다

안에서 네개의 정점으로 늘리는 것으로 평면 폴리곤을 생성, Texture를 붙이는 느낌

1정점분만 로고를 생성(여기서 막지 않으면 정점분량만큼의 로고가 생성된다

로고 데이터(정점, UV)

회전행렬의 산출(할애)...

정점의 생성

원점을 하단에 설정하기 위해 오프셋을 엇갈리게 하여 회전시킨다

Y스케일의 반쯤의 오프셋을 엇갈리게 하여 회전을 걸고, 원래의 위치로 되돌린다

여기에 생성한 원점에 대한 좌표를 생성

60 of 61

▼ 지오메트리 쉐이더 정리

・어디까지나 구현의 한가지 예시이기는 하나, 하는 것은 단순하다

→ (반복하게 되지만)지금까지 정점 쉐이더에서 하고 있었던�  애니메이션과 MVP변환을 정점 쉐이더에서 하지 않고�  지오메트리 쉐이더 쪽에 생성한 정점에 적용하고 있는 느낌

vert

frag

MVP변환

vert

geom

frag

(정점을 생성하여)MVP변환

변환하지 않는다(입력하지 않는다)

【기존】

【지오메트리 쉐이더】

61 of 61

END