Presentation is loading. Please wait.

Presentation is loading. Please wait.

7장 디스플레이 리스트.

Similar presentations


Presentation on theme: "7장 디스플레이 리스트."— Presentation transcript:

1 7장 디스플레이 리스트

2 디스플레이 리스트 디스플레이 리스트는 나중에 다시 사용하게 될 경우에 대비하여 저장해두는 리스트
리스트에 있는 커맨드들은 요청되었던 순서에 따라 실행 내용 디스플레이 리스트를 사용하는 이유 디스플레이 리스트를 사용하는 예제 디스플레이 리스트 설계 방침 디스플레이 리스트의 생성과 실행 다중 디스플레이 리스트의 실행 디스플레이 리스트를 가지고 상태 값들 다루기 2009-2학기 가상현실

3 디스플레이 리스트를 사용하는 이유 디스플레이 리스트는 재사용될 커맨드들을 저장하는데 사용 성능을 향상 시켜줌
디스플레이 리스트는 서버 상태의 일부, 서버 머신에 존재 네트워크 상의 원격 머신에서 OpenGL 프로그램을 사용할 때 디스플레이 리스트 내에 있는 사용되었던 커맨드들을 반복해서 저장할 경우, 네트워크를 통해 데이터를 전송할 때 비용 줄임 로컬머신 에서는 디스플레이 리스트에 사용한 커맨드들을 주기적으로 저장, 성능을 개선 2009-2학기 가상현실

4 예제 7-1 디스플레이 리스트 생성하기: torus.c
#include <GL/glut.h> #include <stdio.h> #include <math.h> #include <stdlib.h> #define PI_ GLuint theTorus; /* 원통을 그린다. */ static void torus(int numc, int numt){ int i, j, k; double s, t, x, y, z, twopi; 2009-2학기 가상현실

5 for (i = 0; i < numc; i++) { glBegin(GL_QUAD_STRIP);
twopi = 2 * PI_; for (i = 0; i < numc; i++) { glBegin(GL_QUAD_STRIP); for (j = 0; j <= numt; j++) { for (k = 1; k >= 0; k--) { s = (i + k) % numc + 0.5; t = j % numt; x = (1+.1*cos(s*twopi/numc))*cos(t*twopi/numt); y = (1+.1*cos(s*twopi/numc))*sin(t*twopi/numt); z = .1 * sin(s * twopi / numc); glVertex3f(x, y, z); } } glEnd(); } 2009-2학기 가상현실

6 /* 디스플레이 리스트를 원통형으로 생성하고 상태를 초기화 한다 */ static void init(void) {
theTorus = glGenLists (1); glNewList(theTorus, GL_COMPILE); torus(8, 25); glEndList(); glShadeModel(GL_FLAT); glClearColor(0.0, 0.0, 0.0, 0.0); } 2009-2학기 가상현실

7 glClear(GL_COLOR_BUFFER_BIT); glColor3f (1.0, 1.0, 1.0);
void display(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3f (1.0, 1.0, 1.0); glCallList(theTorus); glFlush(); } void reshape(int w, int h) glViewport(0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); 2009-2학기 가상현실

8 gluPerspective(30, (GLfloat) w/(GLfloat) h, 1.0, 100.0);
glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(0, 0, 10, 0, 0, 0, 0, 1, 0); } /* x 누르면 x축 회전, y누르면 y축 회전, I 는 원통형을 처음으로 돌림 */ void keyboard(unsigned char key, int x, int y){ switch (key) { case 'x': case 'X': glRotatef(30.,1.0,0.0,0.0); glutPostRedisplay(); break; 2009-2학기 가상현실

9 glutPostRedisplay(); break; case 'i': case 'I': glLoadIdentity();
case 'y': case 'Y': glRotatef(30.,0.0,1.0,0.0); glutPostRedisplay(); break; case 'i': case 'I': glLoadIdentity(); gluLookAt(0, 0, 10, 0, 0, 0, 0, 1, 0); case 27: exit(0); break; } } 2009-2학기 가상현실

10 int main(int argc, char **argv) { glutInitWindowSize(200, 200);
glutInit(&argc, argv); glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); glutCreateWindow(argv[0]); init(); glutReshapeFunc(reshape); glutKeyboardFunc(keyboard); glutDisplayFunc(display); glutMainLoop(); return 0; } 2009-2학기 가상현실

11 실행결과미 2009-2학기 가상현실

12 디스플레이 리스트 설계 방침 성능을 최적화 하기 위해서는 디스플레이 리스트가 동적인 데이터 베이스가 아니라 커맨드들의 저장소가 되어야 함 디스플레이 리스트는 한번 생성되면 수정 불가능 의미 행렬연산(3장 참고) 대부분의 행렬 연산들은 역행렬을 계산하도록 요구 계산될 행렬과 역행렬은 디스플레이 리스트에 있는 특정한 구문에 따라서 저장 래스터 비트맵과 이미지들(8장 참고) 래스터 데이터의 포맷은 하드웨어에 가장 이상적인 형태 아님 하나의 디스플레이 리스트가 컴파일될 때 데이터들을 하드웨어가 선호하는 표현으로 변형적 2009-2학기 가상현실

13 디스플레이 리스트 설계 방침 라이트, 재질 속성, 라이트 모델(5장 참조)
복잡하게 라이팅 된 장면을 그릴 때 장면에 있는 각 물체들의 재질을 바꿀 수 있음 디스플레이 리스트 안에 재질들의 정의를 포함 이 계산들은 재질들을 교체할 때마다 매번 수행될 필요가 없으며, 계산의 결과들만을 저장하면 됨 텍스처(9장 참조) 디스플레이 리스트가 제공된 텍스처 메모리 안에 텍스처 이미지를 저장하는 것을 허용 텍스처를 정의할 때마다 디스플레이 리스트에 넣어서 컴파일하면 효과적 폴리곤 스티플 패턴들(2장 참조) 2009-2학기 가상현실

14 어떠한 변형을 오브젝트에 적용하고 결과를 그릴 때 glNewList(1, GL_COMPILE);
draw_some_geometric_objects(); glEndList(); glLoadMatrix(M); glCallList(1); 오브젝트들이 매번 같은 방식으로 변형되는 경우에는 디스플레이 리스트 안에 행렬을 저장하는 것이 성능 개선 2009-2학기 가상현실

15 디스플레이 리스트의 생성과 실행 glNewList(), glEndList()는 디스플레이 리스트의 시작과 끝을 정의하는데 사용
리스트를 식별하는 인덱스 값을 제공하는 glCallList() 함수를 불러냄 예제 7-2 디스플레이 리스트의 사용: list.c #include <GL/glut.h> #include <stdlib.h> GLuint listName; 2009-2학기 가상현실

16 static void init (void) { listName = glGenLists (1);
glNewList (listName, GL_COMPILE); glColor3f (1.0, 0.0, 0.0); /* 핸재 컬러는 빨간색 */ glBegin (GL_TRIANGLES); glVertex2f (0.0, 0.0); glVertex2f (1.0, 0.0); glVertex2f (0.0, 1.0); glEnd (); glTranslatef (1.5, 0.0, 0.0); /* 위치를 옮김 */ glEndList (); glShadeModel (GL_FLAT); } 2009-2학기 가상현실

17 static void drawLine (void) { glBegin (GL_LINES);
glVertex2f (0.0, 0.5); glVertex2f (15.0, 0.5); glEnd (); } void display(void) GLuint i; 2009-2학기 가상현실

18 glClear (GL_COLOR_BUFFER_BIT);
glColor3f (0.0, 1.0, 0.0); /* 현재 컬러는 초록색 */ for (i = 0; i < 10; i++) /* 10개의 삼각형을 그림 */ glCallList (listName); drawLine (); /* 선의 색이 초록색인가? 아니다. */ /* 선이 어디에 그려졌는가? */ glFlush (); } void reshape(int w, int h) { glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); 2009-2학기 가상현실

19 gluOrtho2D (0.0, 2.0, -0.5 * (GLfloat) h/(GLfloat) w,
glLoadIdentity(); if (w <= h) gluOrtho2D (0.0, 2.0, -0.5 * (GLfloat) h/(GLfloat) w, 1.5 * (GLfloat) h/(GLfloat) w); else gluOrtho2D (0.0, 2.0 * (GLfloat) w/(GLfloat) h, -0.5, 1.5); glMatrixMode(GL_MODELVIEW); } void keyboard(unsigned char key, int x, int y){ switch (key) { case 27: exit(0); break; } 2009-2학기 가상현실

20 int main(int argc, char** argv) { glutInit(&argc, argv);
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB); glutInitWindowSize(650, 50); glutCreateWindow(argv[0]); init (); glutReshapeFunc (reshape); glutDisplayFunc (display); glutKeyboardFunc (keyboard); glutMainLoop(); return 0; } 2009-2학기 가상현실

21 실행결과 2009-2학기 가상현실

22 디스플레이 리스트 이름짓기와 생성하기 각각의 디스플레이 리스트는 정수형 인덱스 값에 의해서 식별
실수로 리스트를 지우지 않기 위해 사용되지 않은 인덱스들을 알아볼 수 있는 glGenList()를 사용 GLuint glGenLists(GLsizei range); 이전에 사용되었다가 해지된 연속적인 리스트의 인덱스 개수를 range에 할당 반환된 정수값은 비어 있는 연속한 디스플레이 리스트들의 시작 지점을 표시하는 인덱스 사용 가능한 인덱스들이 없거나 range 값이 0일 경우에 0 반환 사용 예 listIndex = glGentlists(1); if (listindex != 0) { glNewList(listindex, GL_COMPILE); …. 2009-2학기 가상현실

23 void glNewList(GLunit list, GLenum mode); 디스플레이 리스트의 시작을 표시
mode에 가능한 값들은 GL_COMPILE, GL_COMPLIE_AND_EXECUTE 이다. void glEndList(void) 디스플레이 리스트의 끝을 표시 2009-2학기 가상현실

24 디스플레이 리스트에 어떤 것들이 저장되는가? 디스플레이 리스트를 만들 때 리스트 안에는 표현을 위한 값들만이 저장
만약 배열에 있는 값들이 연이어 변화되어도 디스플레이 리스트의 값은 변화 되지 않음. 모든 OpenGL 커맨드가 디스플레이 리스트 안에서 추출되어 실행되고 저장되는 것은 아님. 예를 들어 클라이언트의 상태 설정 커맨드들이나, 상태값 회수 커맨드들은 저장할 수 없다. Glfloat color_vector[3] = {0.0, 0.0, 0.0}; glNewList(1, GL_COMPILE); glColor3fv(color_vector); glEndList(); color_vector[0] = 1.0; /* 디스플레이 리스트에 영향을 미치지 않음 */ 2009-2학기 가상현실

25 클라이언트의 상태를 설정하는 커맨드들이나 상태값을 회수하는 커맨드들은 디스플레이 리스트에 저장할 수 없다.
디스플레이 리스트에 저장할 수 없는 커맨드들 클라이언트의 상태를 설정하는 커맨드들이나 상태값을 회수하는 커맨드들은 디스플레이 리스트에 저장할 수 없다. glColorPointer() glFlush() glNormalPointer() glDeleteLists() glGenLists() glPixelStore() glDisableClientState() glGet*() glReadPixels() glEdgeFlagPointer() glIndexPointer() glRenderMode() glEnableClientState() glInterleavedArrays() glSelectBuffer() glFeedbackBuffer() glIsEnabled() glTexCoordPointer() glFinish() glIsList() glVertexPointer() 2009-2학기 가상현실

26 값을 반환하는 커맨드: glGet*() 혹은 glIs*() 등
디스플레이 리스트에 저장될 수 없는 커맨드 이유: OpenGL을 네트워크 상에서 이용할 경우엔 디스플레이 리스트는 서버에 위치하며, 서버는 디스플레이 리스트에 관련된 정보를 클라이언트에 의존하지 못하므로. 값을 반환하는 커맨드: glGet*() 혹은 glIs*() 등 클라이언트의 상태를 변화시키는 커맨드: glPixelStore(), glSelectBuffer() 점 배열을 정의하는 커맨드, 클라이언트 상태 포인터: glVertexPointer(), glColorPointer(), glInterleavedArrays() 등 클라이언트 상태에 의존하는 glFlush() 나 glFinish() 2009-2학기 가상현실

27 디스플레이 리스트의 실행 디스플레이 리스트는 생성한 후 glCallList()를 호출해서 실행.
void glCallList(GLuint list) 이 루틴은 디스플레이 리스트를 list를 이용하여 실행 디스플레이 리스트에 접근이 가능한 OpenGL 문법이라면 프로그램의 어느 위치에서도 glCallList() 호출 가능 2009-2학기 가상현실

28 계층적 디스플레이 리스트 glNewList(), glEndList() 사이에 glCallList()를 호출하여 다른 디스플레이 리스트를 실행하는 계층적 디스플레이 리스트를 생성할 수 있다. 계층적 디스플레이 리스트는 특히 한 번 이상 사용되는 구성 요소로 구성된 물체에 대해서 유용하게 쓸 수 있다. 예) 자전거의 일부분을 표현하는 다른 디스플레이 리스트를 호출해서 전체를 렌더링 하는 경우 glNewList(listIndex, GL_COMPILE); glCallList(handlebars); glCallList(frame); glTranslatef(1.0, 0.0, 0.0); glCallList(wheel); glTranslatef(3.0, 0.0, 0.0); glEndList(); 2009-2학기 가상현실

29 예제 7-3 계층적 디스플레이 리스트 디스플레이 리스트에 하나의 도형을 넣고 이 도형의 정점을 쉽게 편집하는 방법
glNewList(1, GL_COMPILE); glVertex3fv(v1); glEndList(); glNewList(2, GL_COMPILE); glVertex3fv(v2); glNewList(3, GL_COMPILE); glVertex3fv(v3); glNewList(4, GL_COMPILE); glBegin(GL_POLYGON); glCallList(1); glCallList(2); glCallList(3); glEnd(); 2009-2학기 가상현실

30 디스플레이 리스트 참조값들 다루기 지금까지 glGenLists()를 호출할 때 아직 사용하지 않은 디스플레이 리스트의 인덱스 값을 사용하는 방법을 사용 glGenLists()를 사용하지 않을 경우, gllsList() 함수를 이용하여 현재 사용되고 있는 특정한 인덱스 값인지를 판별할 수 있음 GLboolean glIsList(GLuint list); GL_TRUE는 리스트에서 이미 사용된 list일 경우 반환 GL_FALSE는 그 반대의 경우이다. void glDeleteLists(GLuint list, GLsizei range); glDeleteLists()를 이용해서 특정한 디스플레이 리스트나 리스트들의 연속된 범위를 확실하게 구분 glDeleteLists()를 사용하면 위의 인덱스 값들이 사용 가능하게 바뀜 List로 특정화된 인덱스 값에서 시작하는 디스플레이 리스트의 range를 삭제 2009-2학기 가상현실

31 다중 디스플레이 리스트의 실행 여러 개의 디스플레이 리스트들을 실행하는 방법을 제공
예를 들어 폰트를 생성할 때 각 디스플레이 리스트 인덱스 값은 그 폰트 안에 있는 문자값의 ASCII값에 부합. 이러한 폰트를 여러 개 표현하려면 각 폰트에 대해서 각기 다른 디스플레이 리스트 인덱스 값을 만들 필요가 있다 초기 인덱스 값은 glCallList()의 호출 전에 glListBase()를 사용하여 표시 void glListBase(GLuint base); 디스플레이 리스트의 base 기본값은 0 base는 하나의 디스플레이 리스트만을 실행하는 glCallList()에 어떠한 영향을 주지않음 2009-2학기 가상현실

32 void glCallList(GLsizei n, GLenum type,const GLvoid *lists):
디스플레이 리스트의 인덱스 값들은 현재의 디스플레이 리스트의 list 배열 포인터에서 부호 있는 정수 값, 목록 base에 의해서 지정된 오프셋을 더하여 계산 type 매개변수는 list에 있는 값들의 데이터 타입을 나타냄 GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT 2009-2학기 가상현실

33 예제 7-5 강조된 폰트를 정의하는 다중 디스플레이 리스트들: stroke.c
#include <GL/glut.h> #include <stdlib.h> #include <string.h> #define PT 1 #define STROKE 2 #define END 3 typedef struct charpoint { GLfloat x, y; int type; } CP; 2009-2학기 가상현실

34 {5, 9, PT}, {5, 0, STROKE}, {0, 5, PT}, {5, 5, END} }; CP Edata[] = {
CP Adata[] = { { 0, 0, PT}, {0, 9, PT}, {1, 10, PT}, {4, 10, PT}, {5, 9, PT}, {5, 0, STROKE}, {0, 5, PT}, {5, 5, END} }; CP Edata[] = { {5, 0, PT}, {0, 0, PT}, {0, 10, PT}, {5, 10, STROKE}, {0, 5, PT}, {4, 5, END} CP Pdata[] = { {0, 0, PT}, {0, 10, PT}, {4, 10, PT}, {5, 9, PT}, {5, 6, PT}, {4, 5, PT}, {0, 5, END} 2009-2학기 가상현실

35 {0, 0, PT}, {0, 10, PT}, {4, 10, PT}, {5, 9, PT}, {5, 6, PT},
CP Rdata[] = { {0, 0, PT}, {0, 10, PT}, {4, 10, PT}, {5, 9, PT}, {5, 6, PT}, {4, 5, PT}, {0, 5, STROKE}, {3, 5, PT}, {5, 0, END} }; CP Sdata[] = { {0, 1, PT}, {1, 0, PT}, {4, 0, PT}, {5, 1, PT}, {5, 4, PT}, {4, 5, PT}, {1, 5, PT}, {0, 6, PT}, {0, 9, PT}, {1, 10, PT}, {4, 10, PT}, {5, 9, END} 2009-2학기 가상현실

36 /* drawLetter() 는 문자와 선 세그먼트들로 표현된 문자들의 배열로부* 터 명령들을 번역한다. */
static void drawLetter(CP *l) { glBegin(GL_LINE_STRIP); while (1) { switch (l->type) { case PT: glVertex2fv(&l->x); break; 2009-2학기 가상현실

37 glVertex2fv(&l->x); glEnd(); glBegin(GL_LINE_STRIP); break;
case STROKE: glVertex2fv(&l->x); glEnd(); glBegin(GL_LINE_STRIP); break; case END: glTranslatef(8.0, 0.0, 0.0); return; } l++; 2009-2학기 가상현실

38 static void init (void) { GLuint base; glShadeModel (GL_FLAT);
base = glGenLists (128); glListBase(base); glNewList(base+'A', GL_COMPILE); drawLetter(Adata); glEndList(); glNewList(base+'E', GL_COMPILE); drawLetter(Edata); glEndList(); glNewList(base+'P', GL_COMPILE); drawLetter(Pdata); glEndList(); glNewList(base+'R', GL_COMPILE); drawLetter(Rdata); glEndList(); glNewList(base+'S', GL_COMPILE); drawLetter(Sdata); glEndList(); glNewList(base+' ', GL_COMPILE); glTranslatef(8.0, 0.0, 0.0); glEndList(); } 2009-2학기 가상현실

39 char *test1 = "A SPARE SERAPE APPEARS AS";
char *test2 = "APES PREPARE RARE PEPPERS"; static void printStrokedString(char *s) { GLsizei len = strlen(s); glCallLists(len, GL_BYTE, (GLbyte *)s); } 2009-2학기 가상현실

40 3 void display(void) { glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1.0, 1.0, 1.0); glPushMatrix(); glScalef(2.0, 2.0, 2.0); glTranslatef(10.0, 30.0, 0.0); printStrokedString(test1); glPopMatrix(); glTranslatef(10.0, 13.0, 0.0); printStrokedString(test2); glFlush(); } 2009-2학기 가상현실

41 void reshape(int w, int h){
glViewport(0, 0, (GLsizei) w, (GLsizei) h); glMatrixMode (GL_PROJECTION); glLoadIdentity (); gluOrtho2D (0.0, (GLdouble) w, 0.0, (GLdouble) h); } void keyboard(unsigned char key, int x, int y){ switch (key) { case ' ': glutPostRedisplay(); break; case 27: exit(0); } 2009-2학기 가상현실

42 int main(int argc, char** argv) { glutInit(&argc, argv);
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB); glutInitWindowSize (440, 120); glutCreateWindow ("stroke"); init (); glutReshapeFunc(reshape); glutKeyboardFunc(keyboard); glutDisplayFunc(display); glutMainLoop(); return 0; } 2009-2학기 가상현실

43 실행결과 2009-2학기 가상현실

44 디스플레이 리스트로 상태 변수 관리 디스플레이 리스트는 OpenGL의 상태 변수들의 값을 변화시키는 호출을 포함할 수 있다.
디스플레이 리스트의 실행에 따라서 변함 명령이 즉각적인 모드에서 호출되는 것처럼 디스플레이 리스트의 실행이 끝난 후에도 변화된 값이 유지 다른 경우에는 디스플레이 리스트를 실행 하기 전에 상태 변수들의 값을 저장. 이 값들을 리스트가 실행된 후에 복구 디스플레이 리스트에서는 glGet*() 사용 불가, 상태변수의 값들을 요청하고 저장하기 위해서는 다른 방법을 사용해야 함 상대 변수들의 그룹을 저장하기 위해서glPushAttrib()사용 값들을 복구하기를 원할 경우 glPopAttrib() 사용 현재의 행렬을 저장하고 복구하기 위해서는 glPushMatrix()와 glPopMatrix()를 사용 2009-2학기 가상현실

45 예제 7-6 디스플레이 리스트의 실행 후 상태 변화의 지속
glNewList(listIndex, GL_COMPILE); glColor3f(1.0, 0.0, 0.0); glBegin(GL_POLYGON); glVertex2f(0.0, 0.0); glVertex2f(1.0, 0.0); glVertex2f(0.0, 1.0); glEnd(); glTranslatef(1.0, 0.0, 0.0); glEndList(); glCallList(listIndex); glBegin(GL_LINES); glVertex2f(1.0, -1.0); /* 컬러값과 행렬이 변화됨 */ 2009-2학기 가상현실

46 예제 7-7 디스플레이 리스트 안에서 상태값들 복구하기
glNewList(listIndex, GL_COMPILE); glPushMatrix(); glPushAttrib(GL_CURRENT_BIT); glColor3f(1.0, 0.0, 0.0); glBegin(GL_POLYGON); glVertex2f(0.0, 0.0); glVertex2f(1.0, 0.0); glVertex2f(0.0, 1.0); glEnd(); glTranslatef(1.5, 0.0, 0.0); glPopAttrib(); glPopMatrix(); glEndList(); 2009-2학기 가상현실

47 모드 변화 캡슐화 여러가지 모드를 변화하거나, 여러 개의 매개변수들을 설정하기 위해 커맨드를 조직하고 저장하는 디스플레이 리스트 사용 디스플레이 리스트는 여러 개의 라이팅, 라이팅 모델, 재질 매개변수 설정들 중에서 교체를 할 경우에 즉각적인 모드보다 효과적 스티플 패턴이나 안개 매개변수, 클리핑 평면 방정식에서 디스플레이 모드를 사용할 수 있다. 2009-2학기 가상현실

48 예제 7-9 모드를 바꾸는 디스플레이 리스트 Gluint offset; offset = glGenList(3)
glNewList(offset, GL_COMPILE); glDisable(GL_LINE_STIPPLE); glEndList(); glNewList(offset+1, GL_COMPILE); glEnable(GL_LINE_STIPPLE); glLineStipple(1, 0x0F0F); glNewList(offset+2, GL_COMPILE); glLineStipple(1, 0x1111); 2009-2학기 가상현실

49 #define drawOneLine(x, y1, x2, y2) glBegin(GL_LINES); \
glVertex2f((x1), (y1); glVertex2f((x2), (y2)); glEnd(); glCallList(offset); drawOneLine(50.0, 125.0, 350.0, 125.0); glCallList(offset+1); drawOneLine(50.0, 100.0, 350.0, 100.0); glCallList(offset+2); drawOneLine(50.0, 75.0, 350.0, 75.0); 2009-2학기 가상현실

50 2009-2학기 가상현실


Download ppt "7장 디스플레이 리스트."

Similar presentations


Ads by Google