WebGL 시작하기

mango·2023년 8월 12일
2

🪡WebGL

목록 보기
2/5

출처: webglfundamentals.org 사이트
해당 사이트의 내용과 설명이 필요한 개념들을 추가하여 재구성한 내용입니다.


WebGL

WebGL 은 종종 3D API로 여겨진다.
사실 WebGL 은 래스터화 엔진에 불과하다.

  • 래스터화 Rasterization
    • 3D 그래픽을 2D 화면에 표시하기 위해 정점을 픽셀로 변환하는 과정

WebGL 은 컴퓨터에 있는 GPU에서 실행된다.
따라서 GPU에서 실행되는 코드를 제공해야 한다.
해당 코드는 함수 쌍 형태로 제공해야 한다.

두 함수는 Vertex ShaderFragment Shader 라고 불리고 C/C++ 처럼 엄격한 타입을 가지는 GLSL로 작성되어 있다.

- GLSL : OpenGL Shading Language

두 개를 합쳐 프로그램 이라고 부른다.

Vertex Shader (정점 셰이더) 의 역할은 정점 위치를 계산하는 것이다.

WebGL은 함수가 출력하는 위치를 기반으로 점, 선, 삼각형 등의 다양한 종류의 Primitive를 래스터화 할 수 있다.

- Primitive: 그래픽스의 기본적인 기하학 요소. 점, 선, 삼각형, 사각형, 폴리곤

래스터화할 때 프리미티브는 Fragment Shader라고 불리는 두 번째 사용자 작성 함수를 호출한다.

Fragment Shader의 역할은 현재 그려지는 프리미티브들의 각 픽셀에 대한 색상을 계산하는 것이다.

대부분의 WebGL API 는 이러한 함수 쌍을 실행하기 위한 상태 설정에 관한 것이다.
원하는 것을 그리기 위해서는 여러 상태를 설정 후, GPU 에서 Shader를 실행하는 gl.drawArraysgl.drawElements 를 호출해서 함수 쌍을 실행한다.

이러한 함수가 접근하는 모든 데이터는 GPU 에 제공되어야 한다.
Shader가 데이터를 받을 수 있는 방법에는 네 가지가 있다.

속성과 버퍼

Buffer 는 GPU에 업로드하는 2진 데이터 배열이다.

버퍼는 크게 두 가지 역할을 한다.

  1. 데이터 저장: 버퍼는 정점 데이터, 텍스처 데이터, 깊이 데이터 등 그래픽스 파이프라인에서 사용되는 다양한 데이터를 저장하는데 사용된다.

  2. 효율적인 데이터 통신: 그래픽스 작업 중에 CPU와 GPU 사이에서 데이터를 전달할 필요가 있을 때, 버퍼를 사용하면 데이터를 효율적으로 이동시킬 수 있다. GPU는 데이터를 병렬로 처리할 수 있기에 버퍼를 통해 대량의 데이터를 효율적으로 처리할 수 있다.

버퍼는 주로 정점 데이터를 담는데 사용되며, 이를 Vertex Buffer 라고 한다.
Vertex Buffer는 보통 정점의 위치, 법선 벡터, 텍스처 좌표, 색상 등을 포함한다.
이 외에도 텍스처 버퍼 등 다양한 종류의 버퍼가 있다.

속성은 버퍼에서 데이터를 가져오고 Vertex Shader 에 제공하는 방법을 지정하는데 사용된다.

예를 들어 위치당 3개의 32비트 부동 소수점으로 위치를 버퍼에 넣을 수 있다고 했을 때,
어느 버퍼에서 위치를 가져올지, 어떤 타입의 데이터를 가져와야 하는지, 버퍼의 오프셋이 어느 위치에서 시작되는지, 한 위치에서 다음 위치로 갈 때 몇 바이트를 이동할건지 등의 속성을 알려줘야 한다.

버퍼는 랜덤하게 접근할 수 없다.
대신에 Vertex Shader가 지정한 횟수만큼 실행된다.
실행될 때마다 지정된 버퍼에서 다음 값을 가져와 속성에 할당한다.

이해를 돕기 위해 단계별로 설명한다.

  1. 버퍼와 랜덤 접근: Buffer 내부의 데이터는 인덱스 순서대로 저장되며, 일반적으로 랜덤하게 특정 위치로 접근하는 것은 어렵다.
  2. Vertex Shader 실행: 버퍼 내의 데이터를 GPU에서 처리하기 위해 Vertex Shader가 사용된다. 정점 데이터는 일렬로 저장되어 있고, 각 정점은 순차적으로 Vertex Shader에 의해 처리된다.
  3. Vertex Shader 실행 횟수: 각 정점마다 Vertex Shader가 실행된다. 모델이 3개의 정점을 가지고 있으면 Vertex Shader는 3번 실행된다.
  4. 속성 할당: Vertex Shader 내에서 각 정점의 속성(위치,색상 등) 을 계산하고 할당한다. 순차 전달이니 1번 정점 데이터는 첫 번째 Vertex Shader 실행에서 읽혀지고, 2번 데이터는 두 번째 실행에서 읽혀지는 식이다.

이렇게 Vertex Shader가 순차적으로 처리해 정점 데이터를 변환하고 필요한 속성 값을 계산해 Fragment Shader로 전달하는 것이다.

유니폼 Uniform

WebGL 에서의 Uniform 유니폼 은 쉐이더에 전달되는 변경 가능한 값이다.

주로 Vertex Shader와 Fragment Shader에서 사용된다.

Uniform 은 카메라 위치, 투영 행렬, 빛의 정보와 같은 전역적인 값 혹은 특정 오브젝트의 특성을 나타내는 값 등을 전달한다.

특징은 다음과 같다.

  1. 변경 가능한 값: 그래픽 작업 도중 언제든지 변경 가능한 값이다. 카메라의 위치나 조명 정보는 매 프레임마다 변경되기에 uniform 을 사용해 Shader에 전달된다.
  2. Shader 간 데이터 공유: 여러 Shader 간에 데이터를 공유하기 위해 사용된다. Vertex Shader와 Fragment Shader 간에 데이터를 전달하는데 사용되며, 이걸 통해 동일한 정보를 사용할 수 있다.
  3. 불변성: 한 번 uniform 이 Shader에 전달되면 해당 Shader내에서는 변경할 수 없다. 변경하려면 CPU 측에서 다시 Shader로 값을 전달해야한다. Shader내에서는 uniform 의 값을 읽기만 가능하다는 소리다.

텍스처 Texture

Texture 텍스처는 Shader 에서 랜덤하게 접근할 수 있는 데이터 배열이다.

텍스처는 2D 이미지 데이터를 나타내는 배열로, 이를 사용해 모델의 표면을 디테일하게 장식하거나 시각적 효과를 적용할 수 있다.

  1. 디테일 및 색상 추가: 모델 표면에 패턴,색상,질감 추가 가능
  2. 조명 및 그림자 시뮬레이션
  3. 텍스처 매핑: 3D 모델 표면에 이미지 매핑

텍스처는 이미지 데이터로 시작하지만 색상 이외의 데이터를 저장하기에도 사용된다.

높이 맵을 텍스처로 사용해 모델의 높낮이 정보를 나타내거나, 특정 지점의 변형 벡터를 저장해 변형 애니메이션을 구현하는 등의 용도로도 활용할 수 있다.

셰이더 프로그램 내에서 텍스처 데이터에 접근하는 것을 Texture Sampling 텍스처 샘플링 이라고 한다.

셰이더 내에서 텍스처 샘플링을 통해 텍스처 데이터를 가져와 표면에 적용하거나 계산에 사용할 수 있다.

Varying

Vertex Shader와 Fragment Shader 간에 데이터를 전달하는 방법이다.

Vertex Shader에서 계산된 값이 Fragment Shader에 전달되고 그 사이에서 값이 보간되어 사용된다.

1. Vertex Shader에서 Varying 설정
2. Fragment Shader에서 보간
3. Fragment Shader에서 사용

WebGL Hello World

WebGL은 클립 공간의 좌표와 색상 두 가지만을 다룬다.
WebGL 을 사용할 때는 이 두 가지를 작성하는거다.

이걸 위해 두 개의 Shader를 제공한다.

클립 공간 좌표를 제공하는 Vertex Shader,
색상을 제공하는 Fragment Shader다.

클립 공간의 좌표는 캔버스 크기에 상관없이 항상 -1에서 +1까지다.

  • 좌표가 항상 X,Y,Z 축을 기반으로 정규화되어서 -1에서 +1 사이의 범위로 표현되는 것

초기화

이제 한번 초기화 코드를 작성해보자.
페이지를 로드할 때 한 번 실행된다.

아래 코드는 Vertex Shader이다.

// 속성은 버퍼에서 데이터를 받습니다.
attribute vec4 a_position;
 
// 모든 셰이더는 main 함수를 가집니다.
void main() {
 
  // gl_Position은 정점 셰이더가 설정을 담당하는 특수 변수
  gl_Position = a_position;
}
  • attribute: 정점 데이터를 나타내는 예약어
  • vec4: 벡터를 나타내는 데이터 타입. vec4는 4개의 요소를 가진 4차원 벡터.
    기본값은 a_position = {x: 0, y: 0, z: 0, w: 1}

다음으로 Fragment Shader가 필요하다.

// 프래그먼트 셰이더는 기본 정밀도를 가지고 있지 않으므로 하나를 선택해야 합니다.
// mediump은 좋은 기본값으로 "중간 정밀도"를 의미합니다.
precision mediump float;
 
void main() {
  // gl_FragColor는 프래그먼트 셰이더가 설정을 담당하는 특수 변수
  gl_FragColor = vec4(1, 0, 0.5, 1); // 자주색 반환
}
  • precision: 부동소수점 수의 연산 정밀도를 설정하는 예약어. 연산 결과의 정확도를 나타냄
  • mediump: 중간 정밀도.

위의 Fragment Shader 코드에서 gl_FragColor

빨강초록파랑투명도
100.51

다음과 같이 설정했다.

다음으로 HTML 캔버스를 설정하고

 <canvas id="c"></canvas>

자바스크립트에서 요소를 잡는다

 var canvas = document.querySelector("#c");


 var gl = canvas.getContext("webgl");
 if (!gl) {
   // webgl이 없어요!
   ...
 }

이걸 이용해 WebGLRenderingContext를 만들 수 있다.

다음은 Shader를 만드는 함수다.

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);
}

이제 두 Shader를 만드는 함수를 호출하자.

var vertexShaderSource = document.querySelector("#vertex-shader-2d").text;
var fragmentShaderSource = document.querySelector("#fragment-shader-2d").text;
 
var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

다음으로 두 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);
}

그리고 호출한다.

var program = createProgram(gl, vertexShader, fragmentShader);

이제 GPU에 GLSL 프로그램을 만들었으니 데이터를 제공해줘야 한다.

WebGL 의 주요 API는 GLSL 프로그램에 데이터를 제공하기 위한 상태 설정에 관한 것이다.

이 경우 GLSL 프로그램에 대한 유일한 입력은 속성인 a_position 이다.

먼저 방금 생성된 프로그램의 속성 위치를 찾는다.

var positionAttributeLocation = gl.getAttribLocation(program, "a_position");

속성 위치를 찾는 건 렌더링할 때가 아니라 초기화 하는 동안 해야한다.

속성은 버퍼에서 데이터를 가져오기에 버퍼도 생성한다.

var positionBuffer = gl.createBuffer();

바인드 포인트는 WebGL의 내부 전역 변수라고 생각하면 된다.
리소스를 바인드 포인트에 바인딩 하면 모든 함수가 바인드 포인트를 통해 리소스를 참조한다.

positionBuffer를 바인딩 해보겠다.

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

이제 바인드 포인트를 통해 해당 버퍼를 참조해 데이터를 넣을 수 있다.

// 2D 포인트 3개
var positions = [
    0, 0,
    0, 0.5,
  0.7, 0,
];
gl.bufferData(
  gl.ARRAY_BUFFER,
  new Float32Array(positions),
  gl.STATIC_DRAW
);
  • gl.bufferData(): 버퍼의 종류를 지정하는 상수, 해당 코드의 경우 배열 버퍼
  • Float32Array() : 32비트 부동 소수점 배열, 해당 코드에서는 positions 배열을 변환하여 담고 있음
  • gl.STATIC_DRAW: 버퍼의 사용 패턴을 지정하는 상수, 정점 데이터가 변하지 않을 것을 나타냄. 이 값은 WebGL에게 최적화된 버퍼 사용 방법을 선택하도록 도와줌

해당 코드를 통해 positions 배열에 저장된 2D 포인트의 좌표 데이터를 WebGL 버퍼에 업로드해서 그래픽스 파이프라인에서 사용할 수 있게 한다.

이러한 데이터는 Vertex Shader에서 처리되어 화면에 렌더링될 때 사용된다.

렌더링

그리기 전에 Canvas 크기를 디스플레이 크기에 맞게 조절해야 한다.

css를 사용하는 방법이 다른 방법보다 더 유연하기에 css로 캔버스 크기를 설정한다.

Canvas의 기본 크기는 400x300 픽셀인데 iframe 내부라면 사용 가능한 공간을 채우기 위해 늘어난다.
이것을 css로 크기를 결정해 이 크기와 일치하도록 조정할 수 있다.

webglUtils.resizeCanvasToDisplaySize(gl.canvas);

다음은 gl_Position 으로 설정할 클립 공간 값을 어떻게 화면 공간으로 변환하는지 WebGL에 알려준다.

이를 위해 gl.viewport 를 호출해서 현재 캔버스 크기를 전달해야 한다.

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  • 0,0: 뷰포트의 시작점인 좌측 하단 모서리 X,Y 좌표 지정
  • gl.canvas.width, :height: 뷰포트는 캔버스의 전체 크기에 맞게 설정됨
// 캔버스 지우기
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);

// 프로그램(셰이더 쌍) 사용
gl.useProgram(program);

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

캔버스를 지우고 실행할 셰이더 프로그램을 WebGL에 알려준 후에 속성 활성화를 해주었다.

다음은 데이터를 어떻게 꺼낼지 지정한다.

// 위치 버퍼 할당
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
 
// positionBuffer(ARRAY_BUFFER)의 데이터를 꺼내오는 방법을 속성에 지시
var size = 2;          // 반복마다 2개의 컴포넌트
var type = gl.FLOAT;   // 데이터는 32비트 부동 소수점
var normalize = false; // 데이터 정규화 안 함
var stride = 0;        // 0 = 다음 위치를 가져오기 위해 반복마다 size * sizeof(type) 만큼 앞으로 이동
var offset = 0;        // 버퍼의 처음부터 시작
gl.vertexAttribPointer(
  positionAttributeLocation,
  size,
  type,
  normalize,
  stride,
  offset
);

gl.vertexAttribPointer 의 숨겨진 부분은 현재 바인딩 된 ARRAY_BUFFER를 속성에 할당한다는 뜻.

이 속성은 이제 positionBuffer에 바인딩 된거다.

이건 ARRAY_BUFFER 바인드 포인트에 다른 걸 자유롭게 바인딩할 수 있다는건데 다른걸 바인딩해도 속성은 계속해서 positionBuffer를 사용한다.

attribute vec4 a_position;

var primitiveType = gl.TRIANGLES;
var offset = 0;
var count = 3;
gl.drawArrays(primitiveType, offset, count);

이제 GLSL 프로그램을 실행하도록 WebGL에 요청할 수 있다.

count 가 3 이니 Vertex Shader를 세 번 실행한다.

먼저 Vertex Shader 속성의 a_position.xa_position.ypositionBbuffer 의 첫 두 개의 값으로 설정된다.

두 번째로 a_position.xa_position.y 가 그 다음 두 개의 값으로 설정된다.

primitiveTypegl.TRIANGLES 로 설정했기에 Vertex Shader가 3번 실행될 때마다, WebGL은 gl_Position에 설정한 3개의 값을 기반으로 삼각형을 그린다.

이때 이 값들은 캔버스 크기에 상관없이 -1~1 사이의 클립 공간 좌표 안에 있다.

 클립 공간        화면 공간
   0, 0     ->   200, 150
   0, 0.5   ->   200, 225
   0.7, 0     ->   340, 150

캔버스 크기가 400x300 이라면 이런식으로 클립 공간을 화면 공간으로 변환한다.

이제 WebGL은 삼각형을 렌더링한다.

그리려는 모든 픽셀에 대해 WebGL은 Fragment Shader를 호출한다.

Fragment Shader는 gl_FragColor1, 0, 0.5, 1 로 설정한다.

Canvas는 채널당 8비트여서 이는 WebGL이 [255, 0, 127, 255] 를 Canvas에 쓴다는걸 의미한다.

삼각형이 중앙에서 시작하는 이유

x의 클립 공간은 -1 ~ 1 의 값을 가진다.
즉 0이 중앙이고 양수 값이 오른쪽이다.

위쪽에 있는 이유는 클립 공간에서 -1은 아래쪽, +1은 위쪽에 있기 때문이다.
함수 사분면 생각하면 된다.

다른 사각형도 호출

먼저 Fragment Shader가 색상 유니폼 입력을 가져오도록 한다.

  precision mediump float;
 
  uniform vec4 u_color;
 
  void main() {
    gl_FragColor = u_color;
  }

그리고 임의의 위치와 임의의 색상 50개의 사각형을 그리는 코드를 써보자.

var colorUniformLocation = gl.getUniformLocation(program, "u_color");
...
 
// 임의의 색상으로 임의의 사각형 50개 그리기
for (var ii = 0; ii < 50; ++ii) {
  // 임의의 사각형 설정
  // ARRAY_BUFFER 바인드 포인트에 마지막으로 바인딩한 것이므로 positionBuffer에 작성됩니다.
  setRectangle(
    gl,
    randomInt(300),
    randomInt(300),
    randomInt(300),
    randomInt(300)
  );
 
  // 임의의 색상 설정
  gl.uniform4f(
    colorUniformLocation,
    Math.random(),
    Math.random(),
    Math.random(),
    1
  );
 
  // 사각형 그리기
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}
 
// 0부터 -1사이 임의의 정수 반환
function randomInt(range) {
  return Math.floor(Math.random() * range);
}
 
// 사각형을 정의한 값들로 버퍼 채우기
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
 
  // 참고: gl.bufferData(gl.ARRAY_BUFFER, ...)는 ARRAY_BUFFER 바인드 포인트에 바인딩된 버퍼에 영향을 주지만 지금까지는 하나의 버퍼만 있었습니다.
  // 두 개 이상이라면 원하는 버퍼를 ARRAY_BUFFER에 먼저 바인딩해야 합니다.
  gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
      x1, y1,
      x2, y1,
      x1, y2,
      x1, y2,
      x2, y1,
      x2, y2
    ]),
    gl.STATIC_DRAW
  );
}

1.var colorUniformLocation = gl.getUniformLocation(program, "u_color"): u_color 라는 이름의 uniform 변수의 위치를 가져온다.

2.gl.uniform4f(colorUniformLocation, Math.random(), Math.random(), Math.random(), 1): u_color 라는 uniform 변수에 값을 설정한다.

3.gl.drawArrays(gl.TRIANGLES, 0, 6): 현재 설정된 버퍼에서 정점 데이터를 읽어와 삼각형을 그린다. 0-6까지의 인덱스 범위에 해당하는 정점 데이터를 사용해 사각형을 그릴 수 있다.
6개의 정점 데이터를 사용하면 3개의 정점으로 이루어진 삼각형 두 개를 그려 사각형을 만든다.

이렇게 WebGL 을 시작해보았다.

다음으로는 셰이더를 자세히 살펴보겠다.


profile
https://mangode.tistory.com/

0개의 댓글