WebGL 정리

안수현·2023년 3월 15일
2
post-thumbnail

WebGL 소개

WebGL은 웹에서 사용 가능한 래스터화 엔진입니다. 주로 3D 그래픽 처리에 사용되지만, GPGPU 등에도 사용될 수 있습니다. 단, WebGL은 그 자체로 3D API가 아닙니다. WebGL로 3D 처리를 할려면 Three.jsBabylon.js 등의 라이브러리를 쓰거나 직접 구현해야 합니다.

현재 WebGL의 버전으로는 WebGL1 과 WebGL2가 있습니다. 기본적으로 역호환도 됩니다. 그 외에도 GPU 관련해서 최근 지원되는 브라우저가 생기고 있는 WebGPU 도 있습니다.

작동 원리

점, 선, 삼각형

WebGL은 점, 선, 삼각형을 이용해 모든 것을 그립니다.

gl.drawArrays나 gl.drawElements의 첫 번째 전달인자를 기반으로 WebGL은 점, 선, 또는 삼각형을 그립니다. 전달 가능한 주요 값들은 아래와 같습니다. 더 많은 목록은 이 글을 참고하세요.

  • POINTS 정점 셰이더가 출력하는 각 클립 공간 정점에 대해 해당 점의 중앙에 정사각형을 그립니다.
  • LINES 정점 셰이더가 출력하는 두개의 클립 공간 정점에 대해 그 두 점을 연결하는 선을 그립니다.
  • TRIANGLES 정점 셰이더가 출력하는 세개의 클립 공간 정점마다 그 점 세개로 삼각형을 그립니다.

클립 공간

WebGL은 클립 공간의 좌표와 색상 두 가지를 다룹니다.

클립 공간에서 좌표는 X축과 Y 축 모두 캔버스 크기에 상관없이 항상 -1에서 +1의 범위를 갖습니다.

전달 방법

셰이더가 데이터를 전달받을 수 있는 방법은 아래와 같습니다.

  1. 버퍼(Buffers)는 GPU에 올라가는 바이너리 데이터 배열입니다. Attribute는 어떻게 버퍼에서 데이터를 가져올지, 그리고 정점 셰이더에 어떻게 전달할지를 명시하기 위해 사용됩니다.
    각각의 attribute가 사용할 버퍼가 무엇인지, 버퍼로부터 데이터를 어떻게 추출하는지와 같은 상태는 VAO(Vertex Array Object)에 저장됩니다.
  2. 유니폼(Uniform)은 셰이더 프로그램을 실행하기 전에 설정하는 전역 변수입니다.
  3. 텍스처(Texture)는 셰이더 프로그램에서 무작위 접근이 가능한 데이터 배열입니다. 일반적으로 이미지를 사용하지만, 다른 계산에 목적으로 원하는 2차원 배열을 사용할 수도 있습니다.
  4. 베링(Varying)은 정점 셰이더에서 프래그먼트 셰이더로 데이터를 전달하기 위한 방안입니다.

셰이더

셰이더는 GPU에서 실행되는 프로그램입니다. WebGL에서는 GLSL이라는 언어로 작성됩니다.

그래픽 처리에서 셰이더는 픽셀의 위치와 색상을 계산합니다.

작성시 어떤 방법으로든 자바스크립트 문자열로 나타내면 됩니다. 버전 명시를 위해 맨 앞에 반드시 #version 300 es 를 적어 주어야 합니다. 기본값은 구버전이기 때문입니다.

Vertex shader

버텍스 셰이더(Vertex shader)는 그래픽 처리에서는 각 정점을 반환하기 위해 사용됩니다. 정점 셰이더라고 불리는 경우도 많습니다. 이후 하드웨어/소프트웨어에서 버텍스 셰이더가 반환한 정점을 바탕으로 각각의 픽셀들이 어디 찍힐지 결정됩니다.

Fragment shader

프래그먼트 셰이더(Fragment shader)는 그래픽 처리에서 각 픽셀의 색을 설정하는데 사용됩니다. 픽셀 셰이더라고 불리는 경우도 많습니다.

기초 사용법

삼각형 그리기 예제

WebGL2를 활용해 삼각형을 그리는 예제 코드입니다.

<!doctype html>
<html>
  <head>
    <title>WebGL</title>
  </head>
  <body>
    <canvas id="out"></canvas>
    <script>
    // 단순히 복사하는 정점 셰이더
    let vs = `#version 300 es

    // attribute는 정점 셰이더에 대한 입력(in)입니다.
    // 버퍼로부터 데이터를 받습니다.
    in vec4 pos;
    
    void main() {
      // gl_Position은 정점 셰이더가 설정해 주어야 하는 내장 변수입니다.
      gl_Position = pos;
    }
    `;

    // 붉은색을 출력하는 프래그먼트 셰이더
    let fs = `#version 300 es

    // 프래그먼트 셰이더는 기본 정밀도를 가지고 있지 않으므로 선언을 해야합니다.
    // highp는 기본값으로 적당합니다. "높은 정밀도(high precision)"를 의미합니다.
    precision highp float;
    
    // 프래그먼트 셰이더는 출력값을 선언해야 합니다.
    out vec4 outColor;
 
    void main() {
      // 붉은색으로 출력값을 설정합니다.
      outColor = vec4(1, 0, 0, 1);
    }
    `;

    // 셰이더 컴파일 함수
    function createShader(gl, type, source) {
      var shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (success) {
        return shader;
      }
     
      console.log(gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
    }

    // 셰이더 링크 함수
    function createProgram(gl, vertexShader, fragmentShader) {
      var program = gl.createProgram();
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);
      gl.linkProgram(program);
      var success = gl.getProgramParameter(program, gl.LINK_STATUS);
      if (success) {
        return program;
      }

      console.log(gl.getProgramInfoLog(program));
      gl.deleteProgram(program);
    }

    function main() {
      // WebGL2 가져오기
      var canvas = document.querySelector("#out");
      var gl = canvas.getContext("webgl2");
      if(!gl) {
        // WebGL2 사용 불가시 처리
        document.write("WebGL2 is not supported")
        return -1;
      }

      // GLSL 셰이더 생성
      var vertexShader = createShader(gl, gl.VERTEX_SHADER, vs);
      var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fs);

      // 셰이더를 링크하여 프로그램 생성
      var program = createProgram(gl, vertexShader, fragmentShader);

      // 정점 데이터를 전달할 방법 설정
      var positionAttributeLocation = gl.getAttribLocation(program, "pos");

      // 위치 버퍼 생성 후 ARRAY_BUFFER 에 바인드
      var positionBuffer = gl.createBuffer();

      gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

      // 위치 지정
      var pos = [
        0.5, 1,
        0, 1,
        0, 0.5,
      ];

      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(pos), gl.STATIC_DRAW);

      // vao(vertex array object) 생성
      var vao = gl.createVertexArray();

      // vao 바인드
      gl.bindVertexArray(vao);

      // attribute 활성화
      gl.enableVertexAttribArray(positionAttributeLocation);

      // attribute 설정
      var size = 2;
      var type = gl.FLOAT;
      var normalize = false;
      var stride = 0;
      var offset = 0;

      gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);

      // 클립 공간을 픽셀로 변환할 방법 설정
      gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

      // 캔버스 비우기
      gl.clearColor(0, 0, 0, 0);
      gl.clear(gl.COLOR_BUFFER_BIT);

			// --------------------

      // 사용할 프로그램 지정
      gl.useProgram(program);

			// vao 바인드
      gl.bindVertexArray(vao);

      // 그리기
      var primitiveType = gl.TRIANGLES;
      var offset = 0;
      var count = pos.length / 2;
      
      gl.drawArrays(primitiveType, offset, count);

      return 0;
    }
    window.onload = main;
    </script>
  </body>
</html>

출력은 아래와 같습니다.

GPGPU

GPGPU는 GPU를 그래픽 이외의 산술 연산 목적으로 사용하는 것입니다.

Transform feedback

Transform feedback(XFB)은 vertex shader에서 varrings 출력을 하나 이상의 버퍼에 쓸 수 있는 기능입니다. Transform feedback의 출력은 1차원 형태입니다.

더하기 예제

WebGL2를 활용해 덧셈을 수행하는 예제 코드입니다.

<!doctype html>
<html>
  <head>
    <title>WebGL</title>
  </head>
  <body>
    <canvas id="out"></canvas>
    <script>
    // 덧셈 후 출력하는 정점 셰이더
    let vs = `#version 300 es

    in float a;
    in float b;
    out float sum;

    void main() {
      sum = a + b;
    }
    `;

    // 빈 프래그먼트 셰이더
    let fs = `#version 300 es

    // 프래그먼트 셰이더는 기본 정밀도를 가지고 있지 않으므로 선언을 해야합니다.
    // highp는 기본값으로 적당합니다. "높은 정밀도(high precision)"를 의미합니다.
    precision highp float;
    
    void main() {
    }
    `;

    // 셰이더 컴파일 함수
    function createShader(gl, type, source) {
      var shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (success) {
        return shader;
      }
     
      console.log(gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
    }

    // 버퍼 생성 함수
    function makeBuffer(gl, sizeOrData) {
      const buf = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buf);
      gl.bufferData(gl.ARRAY_BUFFER, sizeOrData, gl.STATIC_DRAW);
      return buf;
    }

    // 버퍼 생성 후 attribute 설정 함수
    function makeBufferAndSetAttribute(gl, data, loc) {
      const buf = makeBuffer(gl, data);
      gl.enableVertexAttribArray(loc);
      gl.vertexAttribPointer(
        loc,
        1,
        gl.FLOAT,
        false,
        0,
        0,
      );
    }

    function main() {
      // WebGL2 가져오기
      var canvas = document.querySelector("#out");
      var gl = canvas.getContext("webgl2");
      if(!gl) {
        // WebGL2 사용 불가시 처리
        document.write("WebGL2 is not supported")
        return -1;
      }

      // GLSL 셰이더 생성
      var vertexShader = createShader(gl, gl.VERTEX_SHADER, vs);
      var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fs);

      // 셰이더를 링크하여 프로그램 생성
      const program = gl.createProgram();
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);
      gl.transformFeedbackVaryings(
        program,
        ["sum"],
        gl.SEPARATE_ATTRIBS,
      );
      gl.linkProgram(program);
      if(!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        throw new Error(gl.getProgramParameter(program));
      }

      const aLoc = gl.getAttribLocation(program, 'a');
      const bLoc = gl.getAttribLocation(program, 'b');

      // vao(vertex array object) 생성
      const vao = gl.createVertexArray();
      
      // vao 바인드
      gl.bindVertexArray(vao);

      // 데이터 설정
      const a = [1, 2, 3, 4, 5, 6];
      const b = [3, 6, 9, 12, 15, 18];

      // 버퍼에 데이터 넣기
      const aBuffer = makeBufferAndSetAttribute(gl, new Float32Array(a), aLoc);
      const bBuffer = makeBufferAndSetAttribute(gl, new Float32Array(b), bLoc);

      // Transform feedback 생성
      const tf = gl.createTransformFeedback();

      // Transform feedback 바인드
      gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
     
      // 출력 버퍼 생성
      const sumBuffer = makeBuffer(gl, a.length * 4);
      
      // 출력 버퍼 바인드
      gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, sumBuffer);

      gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
      gl.bindBuffer(gl.ARRAY_BUFFER, null);

      // --------------------

      // 사용할 프로그램 지정
      gl.useProgram(program);
      
      // vao 바인드
      gl.bindVertexArray(vao);

      // 프래그먼트 셰이더 호출 비활성화
      gl.enable(gl.RASTERIZER_DISCARD);

      // 처리
      gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tf);
      gl.beginTransformFeedback(gl.POINTS);
      gl.drawArrays(gl.POINTS, 0, a.length);
      gl.endTransformFeedback();
      gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);

      // 프래그먼트 셰이더 호출 활성화
      gl.disable(gl.RASTERIZER_DISCARD);

      // 출력
      const results = new Float32Array(a.length);
      gl.bindBuffer(gl.ARRAY_BUFFER, sumBuffer);

      gl.getBufferSubData(
        gl.ARRAY_BUFFER,
        0, // 오프셋
        results,
      );

      document.write("a : ");
      for(let n of a) {
        document.write(n, " ");
      }
      document.write("<br/>");

      document.write("b : ");
      for(let n of b) {
        document.write(n, " ");
      }
      document.write("<br/>");

      document.write("sum : ");
      for(let n of results) {
        document.write(n, " ");
      }
      document.write("<br/>");

      gl.bindBuffer(gl.ARRAY_BUFFER, null);

      return 0;
    }
    window.onload = main;
    </script>
  </body>
</html>

출력은 아래와 같습니다.

내부 구조

ANGLE 소개

Almost Native Graphics Layer Engine

ANGLE는 구글에서 만든 API로, OpenGL ES API를 하드웨어 지원 API(렌더러)로 변환해 줍니다.

Chrome, Edge, Firefox, Safari 등의 주요 브라우저에서 WebGL 구현을 위해 사용됩니다.

현재 지원 상황

ANGLE의 버전/플랫폼/렌더러 지원 현황은 아래와 같습니다.

OpenGL ES 버전별 지원되는 렌더러 현황

플랫폼별 지원되는 렌더러 현황

백엔드 선택

ANGLE에서 백엔드로 사용할 그래픽 API(렌더러)를 선택하기 위해서는 크롬 실행 시 아래와 같은 형태로 명령줄 옵션을 추가해야 합니다. 참고
--use-gl=angle --use-angle=<backend>

폴더 구조

소스코드는 Git 이나 Chromium Code Search 에서 확인할 수 있습니다.

이슈는 Crbug 에서, 최신 커밋은 Chromium Gerrit 에서 확인할 수 있습니다.

  1. Function src/libGLESv2/libGLESv2_autogen.cpp
  2. EntryPoint src/libGLESv2/entry_point_gles*
  3. Validation src/libANGLE/validationES*
  4. Context src/libANGLE/Context*
  5. Backend src/libANGLE/renderer/*

참고자료

ANGLE https://chromium.googlesource.com/angle/angle/
ANGLE : 크롬 공격 벡터 소개 https://www.youtube.com/watch?v=7IklO4tpVGg
WebGL 기초 https://webglfundamentals.org/webgl/lessons/ko/webgl-fundamentals.html
WebGL2 기초 https://webgl2fundamentals.org/webgl/lessons/ko/webgl-fundamentals.html
Browser Hacking With ANGLE conference.hitb.org

profile
초보 해커의 생존기

0개의 댓글