ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OpenGL 끄적임 - 2 Vertices, Shaders
    Graphics/OpenGL ES 2.0 2018. 4. 8. 23:15
    반응형

    Vertex 작성방법

    OpenGL의 도형들은 다 vertex로 시작한다.

    vertex 하나당 (x,y) 

    float[] tableVetices = { 0f, 0f, 0f, 14f, 9f, 14f, 9f, 0f }

    sequential list 를 사용함. 


    각각의 점들이 합쳐져서 삼각형이되고 OpenGL은 점을 어떻게 연결하냐에 따라 재밌는 것들을 볼 수 있다. 

    curve를 표현하기 위해서는 더 많은 점들이 필요하다.


    삼각형을 그리는 순서

    1.반시계방향(counter-clockwise order)으로 그린다. (winding order라고도 한다.)


    아래 기본적인 OpenGL 구동 방식을 이해해야한다.

    1. 에뮬레이터 또는 장치에서 Java 코드를 컴파일하고 실행하면 하드웨어에서 직접 실행되지 않습니다. 대신 Dalvik 가상 시스템이라는 특별한 환경을 실행합니다. 이 가상 컴퓨터에서 실행되는 코드는 특수 API를 통한 경우를 제외하고는 네이티브 환경에 직접 액세스 할 수 없습니다.

    2. Dalvik 가상 시스템은 또한 가비지 수집을 사용합니다. 즉, 가상 컴퓨터가 변수, 개체 또는 다른 일부 메모리가 더 이상 사용되지 않는 것을 감지하면 해당 메모리는 다시 사용 가능하도록 해제됩니다. 또한 공간을보다 효율적으로 사용할 수 있도록 주변을 움직일 수 있습니다.


    네이티브 환경은 동일한 방식으로 작동하지 않으며 메모리 블록이 자동으로 이동되고 해제 될 것으로 기대하지는 않습니다.

    Android는 CPU나 기타 LOW-LEVEL을 알필요 없도록 가비지컬렉션을 사용하도록 설계되어있다. 하지만 우리가 사용할 OpenGL은 native system library처럼 hardware에 직접적으로 접근한다. 그래서 가비지컬렉션을 사용하지 않고 동작한다. 


    그러면 Java는 어떻게 Native codes를 실행하지?

    1) JNI라는 Java Native Interface의 도움을 받는다. 이 method들의 집합은 android.opengl.GLES20 패키지에 모여있다. SDK는 JNI로 native system library를 호출한다. 

    2) 우리 native 메모리 할당을 어떻게 하는가? Java의 일부 코드를 사용해서 native memory로부터 우리가 사용하는 memory에 data를 copy한다. native memory는 native한 환경에 접근가능하다. 이는 가비지컬렉터에의해 관리되지 않는 영역이다. 


    우리는 data를 transfer 해야한다. float은 JAVA에서는 32bit(4byte)이고 이를 모든 곳에서 사용해야한다. 그래서 FloatBuffer라는 data class를 사용하게 된다.

    ex) vertexData = ByteBuffer

              .allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)

              .order(ByteOrder.nativeOrder())

              .asFloatBuffer();

    Float하나당 4byte이고 우리는 length만큼의 vertices를 만들었으므로 위의 갯수만큼 native 메모리를 할당한다. nativeOrder()는 숫자들 저장 순서가 왼쪽에서 오른쪽이냐 오른쪽에서 왼쪽이냐는 의미이고 플랫폼의 순서로 사용한다는 의미로 생각하면 된다. 

    vertexData.put(tableVerticesWithTriangles);

    그리고 위의 코드로 가상 메모리에서 native메모리로 data copy를 할 수 있다. 위 경우에는 프로세스가 destoy되면 메모리가 해제되므로 걱정하지 않아도 된다. 

    우리는 vertex shader각각의 점의 마지막점을 생성하는 subroutine을 가지고 있다. 우리는 또한 각각의 fragment에 대한 마지막 color를 생성하는 subroutine도 만들어야한다. 이 것 전에, fragment가 무엇인지, 어떻게 만들어지는지부터 알아보자.

    -Rasterization (이미지를 화면에 표시하는 기법)

    모바일 디스플레이는 픽셀이라고 알려진 수천개에서 수만개의 조합들이다. 이들은 각각의 다른색상을 표현하기도 하는데 이는 다른 색상이 아니고 R, G, B 로만 표현하는 색상들의 트릭이다. 픽셀은 아주 작아서 눈으로 보여지기에 다양한 색상 트럼으로 표현될 수 있다. 

    아날로그의 이미지를 작은 조각으로 분해해 디스플레이에 표현할 수 있도록 픽셀 단위로 표현하는데 이를 Resterization이라고 한다. fragment는 R,G,B,Alpha라는 색상으로 표현한다.

    우리는 OpenGL이 한 줄을 한 fragment로 맵핑하는 예제를 볼 수 있다. 이때 하나의 fragment를 하나의 pixel로 맵핑한다고들 하는데 항상 그렇지는 않다. 초고해상도 장치는 큰 단위의 fragment를 사용할 수 있어서 큰 fragment로 GPU의 작업량을 줄이는 것이 좋다. 


    Hockey 게임 만들기


    -Hockey 테이블을 작성하기 위해서 

    OpenGL 우리가 그릴 것을 알려줘야한다. OpenGL 이해할 있도록 점으로 알려주는 것이 중요하다. 

    점을 그려줄 Renderer 점에 대한 정보를 알려준다. 후에 color 다른 속성도 아래와 같이 Array 형태로 저장할 있다. 

    AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java 

    public AirHockeyRenderer() {

                float[] tableVertices = {

                        0f,  0f,

                        0f, 14f,

                        9f, 14f,

                        9f,  0f

    } 


    -점들을 표현하기 위해서 

    모든 물체는 삼각형으로 표현할 있다. 물체를 표현하려면 , , 삼각형으로 표현하면 된다. 복잡한 물체를 표현하고 싶을때는 arch 곡선을 표현하면 된다. Hockey table 아래와 같이 작성하면 된다.

    AirHockey1/src/com/airhockey/android/AirHockeyRenderer.java 

    float[] tableVerticesWithTriangles = {

            // Triangle 1

            0f,  0f,

            9f, 14f,

            0f, 14f,

    // Triangle 2 

            0f,  0f,

            9f,  0f,

            9f, 14f

    }


    이미 알아차렸을 수도 있는데, 좌표는 반시계방향으로 작성해야한다. 우리는 반시계방향 방법으로 성능을 좋게 수있다. 뒷면에 보이지 않는 삼각형이라던가 OpenGL 이런 도형은 skip하고 그려달라고 있다. 


    -OpenGL 데이터에 접근하기 위해서 

    Java code hardware에서 직접 동작하지 않고 자바 가상 머신 위에서 동작하기 때문에 native 환경에 접근하려면 특별한 API들을 사용해야한다.

    가상 머신은 garbage collection 사용한다. 의미는 이상 사용되지 않는 object, variable들에 대해서 직접 메모리가 해제되고 재사용될 있도록 한다. 

    하지만 native 환경에서는 메모리가 직접 해제되지 않는다. 안드로이드는 cpu 플랫폼의 구조에 대해서 고민할 필요 없도록 작성되어 있다. OpenGL등의 low단의 메모리 관리에 대한 고민이 필요없기 때문이다. OpenGL Hardware 윗단에서 동작하며 영역에는 가상 머신도 없고 가비지 컬렉션이나 메모리 정리 등의 기능도 없다. 


    -Java에서 Native code 사용

    Java 가상머신 위에서 동작하면, OpenGL 어떻게 접근할까? JNI라는 trick 사용하면 된다. Android SDK라는 녀석은 이미 것을 구현해 놓았다. 이를 android.opengl.GLES20 package 라고 부르며 native system library 부르는 JNI 사용할 있도록 해준다. 


    -Java memory heap으로부터 Native memory heap으로 memory copy하기 위해 

    OpenGL 사용하는 두번째 방법을 사용하기 위해 우선, 우리 메모리를 어떻게 allocate하는지 알아야한다. Java 코드로 native memory allocate하고 native 환경에 접근할 있지만, 이는 가비지 컬렉션이 관리하는 영역이 아니다. data 옮기기위해 다음 그림을 이해해야한다.

    [그림] data 가상머신에서 OpenGL 이동하는 과정 


    우리가 사용할 FloatBuffer 라는 class native memory 데이터를 저장한다. Java에서 float 32bit이고 byte 4byte이다. 


    직접 ByteBuffer라는 class FloatBuffer 할당하기 위해서는 아래와같이 작성한다.

     vertexData = ByteBuffer

              .allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)

              .order(ByteOrder.nativeOrder())

              .asFloatBuffer();

    vertexData.put(tableVerticesWithTriangles);


    할당된 메모리는 가비지컬렉터가 관리하는 범위가 아니다. tableVerticesWithTriangles.length * BYTES_PER_FLOAT 

    우리가 할당할 Array 크기이다. 


    -Endianness?

    Endianness Hardware 구조를 bits byte순서대로 어떻게 작성할지에 대한 방법이다. 

    엔디안은 하드웨어 아키텍처가 낮은 수준에서 숫자를 구성하는 비트와 바이트를 정렬하는 방법을 설명하는 방법입니다. 

    ex) 10 진수 10000 16 진수 시스템에서 2710입니다. 시스템은 때로는 개의 문자가 하나의 8 비트 바이트에 해당하기 때문에 컴퓨터 코드를 작동합니다. 엔디안 아키텍처에서는 다음과 같이이 번호를 저장합니다.

    27 10

    소형 엔디안 아키텍처에서는 동일한 번호가 다음과 같이 저장됩니다.

    10 27

    우리는 일반적으로 엔디안에 대해 걱정할 필요가 없습니다. ByteBuffer 사용할 때는 하드웨어와 동일한 순서를 사용해야합니다. 그렇지 않으면 우리의 결과는 크게 잘못 것입니다. Wikipedia.a에서 엔디안에 대해 많이 읽을 있습니다.


    -shader?

    GPU에게 data 어떻게 그릴건지 알려주는 역할이다. shader 두가지 타입이 있다. 우리는 screen 무언가 그리기 위해서 두가지 타입의 shader 만들 것이다.

    1. 버텍스 쉐이더는 버텍스의 최종 위치를 생성하며 모든 좌표(x,y)마다 한번씩 실행됩니다. 일단 최종 위치가 알려지면 OpenGL 눈에 보이는 꼭지점 집합을 가져 와서 , 삼각형으로 어셈블합니다.

    2. 프레그먼트 쉐이더는 , 또는 삼각형의 조각의 최종 색상을 생성하며 모든 fragment마다 실행됩니다. 프래그먼트는 컴퓨터 화면의 픽셀과 유사한 단일 색상의 작고 직사각형 영역입니다.


    마지막 컬러가 생성되면 GL 컬러를 프레임 버퍼라고 알려진 메모리 블럭에 담고 안드로이드는 프레임버퍼를 화면에 그리게된다.

    http://khronos.org

    라는 사이트에 가면 GL 대해서 많은 체험을 있다.


    OpenGL에서 shader 등장하기 전에는 shader 있는게 적었지만 OpenGL 고정된 API 만드로 확장하기가 어려웠습니다. OpenGL ES 2.0 부터 shader 사용하여 확장 가능한 shader 만들 있게 되었습니다. 모든 점과 모든 fragment들을 컨트롤 있는 shader 만들 있습니다. 가벼운 효과, 깔끔한 효과, 카툰 shading 같은 효과를 사용할 있습니다. 


    -우리의 vertex shader 만들어볼까?

    점의 위치를 지정하는 vertex shader 만들자. 

    AirHockey1/res/raw/simple_vertex_shader.glsl 

    attribute vec4 a_Position;

    void main() { 

        gl_Position = a_Position;

    }


    shader GLSL 이용해 정의한다. 


    vec4는 네 가지 구성 요소로 구성된 벡터다. 위치 컨텍스트에서 네 개의 구성 요소를 위치의 x, y, z 및 w 좌표로 생각할 수 있는데, x, y 및 z는 3D 위치에 해당하고, w는 특별 좌표이다. (추후 배울 예정). 기본 값이 지정되지 않은 경우 OpenGL의 기본 동작은 첫 번째 벡터의 세 좌표 값은 0이고 마지막 좌표 값은 1이다.
    우리는 vertex shader각각의 점의 마지막점을 생성하는 subroutine을 가지고 있다. 우리는 또한 각각의 fragment에 대한 마지막 color를 생성하는 subroutine도 만들어야한다. 이 것 전에, fragment가 무엇인지, 어떻게 만들어지는지부터 알아보자. 

    -Rasterization (이미지를 화면에 표시하는 기법)
    모바일 디스플레이는 픽셀이라고 알려진 수천개에서 수만개의 조합들이다. 이들은 각각의 다른색상을 표현하기도 하는데 이는 다른 색상이 아니고 R, G, B 로만 표현하는 색상들의 트릭이다. 픽셀은 아주 작아서 눈으로 보여지기에 다양한 색상 트럼으로 표현될 수 있다. 
    아날로그의 이미지를 작은 fragment로 분해해 디스플레이에 표현할 수 있도록 픽셀 단위로 표현하는데 이를 Resterization이라고 한다. fragment는 R,G,B,Alpha라는 색상으로 표현한다. 이 R, G, B 모나리자를 그릴 수도 있다.
    우리는 OpenGL이 한 줄을 한 fragment로 맵핑하는 예제를 볼 수 있다. 이때 하나의 fragment를 하나의 pixel로 맵핑한다고들 하는데 항상 그렇지는 않다. 초고해상도 장치는 큰 단위의 fragment를 사용할 수 있어서 큰 fragment로 GPU의 작업량을 줄이는 것이 좋다. 

    우리 코드 작성 
    fragment shader의 주 목적은 fragment 가 가져야하는 색상을 GPU에게 말해주는 것이다. fragment shader는 매번 primitive fragment마다 호출된다. 즉, 10,000개 fragment에 triangle을 그리면 fragment shader는 10,000번 불린다. 

    AirHockey1/res/raw/simple_fragment_shader.glsl
    precision mediump float;
    uniform vec4 u_Color;
    void main() {
        gl_FragColor = u_Color;
    }

    Precision Qualifiers (영어 이해 못해서 번역기)
    정밀 한정자
    파일의 첫 번째 줄은 프래그먼트 셰이더의 모든 부동 소수점 데이터 유형에 대한 기본 정밀도를 정의합니다. 이것은 Java 코드에서 float와 double을 선택하는 것과 같습니다.
    낮은 정밀도, 중간 정밀도 및 높은 정밀도에 해당하는 lowp, mediump 및 highp 중에서 선택할 수 있습니다. 그러나 highp는 일부 구현에서 프래그먼트 셰이더에서만 지원됩니다.
    버텍스 쉐이더에서 왜이 작업을 수행하지 않았습니까? 버텍스 쉐이더는 기본 정밀도를 변경할 수도 있지만, 버텍스의 위치와 관련하여 정확성이 더 중요하기 때문에 OpenGL 디자이너는 버텍스 쉐이더를 기본값 인 highp로 설정하기로 결정했습니다.
    고정밀 데이터 유형이 더 정확하지만 성능 저하로 인해 발생합니다. 프래그먼트 쉐이더의 경우 최대한의 호환성을 위해 속도와 품질 간의 좋은 절충안으로 mediump를 선택합니다.



    **coordinate : 좌표 (ex. x coordinate - x 좌표)

    **vertex = (x,y), vertecies = (x,y, x,y)

    **2Dimension = (x,y), 3Dmension = (x,y,z)

    반응형
Designed by Tistory.