셰이더 소개

소개

앞서 Three.js를 소개해 드린 바 있습니다. 아직 읽지 못했다면 이 문서에서 빌드할 기초가 되므로 그것이 기반이 될 수도 있습니다.

제가 하고 싶은 것은 셰이더에 대해 논의하는 것입니다. WebGL은 훌륭합니다. 앞서 Three.js (및 기타 라이브러리)에서 언급했듯이 어려움을 추상화하는 놀라운 기능을 제공합니다. 하지만 특정 효과를 달성하고 싶거나 환상적인 요소가 화면에 어떻게 표시되는지 좀 더 자세히 알아보고 싶을 때가 있을 것입니다. 셰이더는 거의 확실하게 이러한 방정식의 일부입니다. 저와 같다면 지난 튜토리얼에서 다룬 기본적인 내용에서 좀 더 까다로운 단계로 넘어갈 수도 있습니다. Three.js를 사용한다는 점을 기반으로 작업하겠습니다. 셰이더를 처리하는 측면에서 당나귀가 많은 작업을 수행하기 때문입니다. 앞에서도 처음부터 셰이더의 맥락을 설명하고 이 튜토리얼의 후반부에서 좀 더 고급 영역을 살펴볼 것임을 분명히 밝힙니다. 그 이유는 셰이더가 처음 눈에 띄고 약간의 설명이 필요하기 때문입니다.

1. 두 가지 셰이더

WebGL은 고정 파이프라인의 사용을 제공하지 않습니다. 고정 파이프라인의 사용은 즉시 사용 가능한 렌더링 방법을 제공하지 않는다는 의미입니다. 하지만 제공하는 것은 프로그래밍 가능한 파이프라인입니다. 이 파이프라인은 더 강력하지만 이해하고 사용하기 더 어렵습니다. 간단히 말해 프로그래밍 가능한 파이프라인은 개발자가 화면에 렌더링되는 꼭짓점 등을 가져오는 것을 프로그래머가 책임진다는 것을 의미합니다. 셰이더는 이 파이프라인의 일부이며 두 가지 유형이 있습니다.

  1. 꼭짓점 셰이더
  2. 프래그먼트 셰이더

두 가지 모두 그 자체로는 전혀 의미가 없습니다. 둘 다 그래픽 카드의 GPU에서 전적으로 실행된다는 점을 알아야 합니다. 즉, CPU가 다른 작업을 하도록 하고 가능한 모든 것을 오프로드해야 합니다. 최신 GPU는 셰이더에 필요한 기능에 맞게 최적화되어 있으므로 사용하기에 좋습니다.

2. 버텍스 셰이더

구와 같은 표준 기본 도형을 사용합니다. 이는 꼭짓점으로 이루어져 있습니다. 맞나요? 꼭짓점 셰이더에는 이러한 꼭짓점에 모두 차례로 주어지며 조작할 수 있습니다. 실제로 각 셰이더에서 실제로 실행하는 것처럼 꼭짓점 셰이더에 따라 다르지만, 한 가지 책임이 있습니다. 즉, 특정 지점에서 4D 부동 벡터(화면에서 꼭짓점의 최종 위치)인 gl_Position이라는 것을 설정해야 합니다. 3D 위치 (x,y,z가 있는 꼭짓점)를 2D 화면에 또는 투영하는 것이기 때문에 그 자체로 매우 흥미로운 프로세스입니다. Three.js와 같은 파일을 사용한다면 너무 무겁지 않게 gl_Position을 간단히 설정할 수 있습니다.

3. 프래그먼트 셰이더

이제 꼭짓점이 있는 객체를 가지고 2D 화면에 투영했습니다. 하지만 사용하는 색상은 어떨까요? 텍스처와 조명은 어떨까요? 바로 이러한 경우에 프래그먼트 셰이더가 사용됩니다. 꼭짓점 셰이더와 매우 유사한 프래그먼트 셰이더에도 하나의 필수 작업만 있습니다. 프래그먼트의 최종 색상인 또 다른 4D 부동 벡터인 gl_FragColor 변수를 설정하거나 삭제해야 합니다. 하지만 프래그먼트란 무엇일까요? 삼각형을 만드는 세 개의 꼭짓점을 생각해 보세요. 이 삼각형 내의 각 픽셀을 그려야 합니다. 프래그먼트는 이 삼각형의 각 픽셀을 그리기 위해 세 꼭짓점에서 제공하는 데이터입니다. 따라서 프래그먼트는 구성요소 꼭짓점에서 보간된 값을 수신합니다. 한 꼭짓점이 빨간색이고 이웃이 파란색이면 색상 값이 빨간색에서 보라색에서 파란색으로 보간되는 것을 볼 수 있습니다.

4. 셰이더 변수

변수에 관해 이야기할 때는 Uniforms, Attributes, Varyings의 세 가지 선언을 만들 수 있습니다. 이 세 사람에 대해 처음 들었을 때는 제가 작업한 다른 제품과 전혀 일치하지 않아서 매우 혼란스러웠습니다. 그러나 다음과 같이 생각할 수 있습니다.

  1. 유니폼은 꼭짓점 셰이더와 프래그먼트 셰이더 모두로 전송되며 렌더링되는 전체 프레임에서 동일하게 유지되는 값을 포함합니다. 이에 대한 좋은 예는 조명의 위치일 수 있습니다.

  2. 속성은 개별 꼭짓점에 적용되는 값입니다. 속성은 꼭짓점 셰이더에서만 사용할 수 있습니다. 이것은 고유한 색상을 가진 각 꼭짓점과 같을 수 있습니다. 속성은 꼭짓점과 일대일 관계가 있습니다.

  3. Varying은 프래그먼트 셰이더와 공유할 꼭짓점 셰이더에 선언된 변수입니다. 이렇게 하려면 꼭짓점 셰이더와 프래그먼트 셰이더에서 모두 유형과 이름이 동일한 다양한 변수를 선언해야 합니다. 이것의 일반적인 용도는 꼭짓점의 표준입니다. 이는 조명 계산에 사용될 수 있기 때문입니다.

나중에 이 세 유형을 모두 사용해 실제로 어떻게 적용되는지 알 수 있습니다.

지금까지 꼭짓점 셰이더와 프래그먼트 셰이더, 그리고 이들이 처리하는 변수 유형에 관해 알아봤습니다. 이제 만들 수 있는 가장 간단한 셰이더를 살펴보겠습니다.

5. 본주르노 월드

다음은 꼭짓점 셰이더의 Hello World입니다.

/**
* Multiply each vertex by the model-view matrix
* and the projection matrix (both provided by
* Three.js) to get a final vertex position
*/
void main() {
gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}   

다음은 프래그먼트 셰이더의 경우에도 동일합니다.

/**
* Set the colour to a lovely pink.
* Note that the color is a 4D Float
* Vector, R,G,B and A and each part
* runs from 0.0 to 1.0
*/
void main() {
gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);
}

그다지 복잡하지는 않죠?

꼭짓점 셰이더에서 Three.js가 몇 가지 유니폼을 전송합니다. 이 두 유니폼은 Model-View Matrix와 Projection Matrix라는 4D 행렬입니다. 이러한 작동 방식을 정확히 알 필요는 없습니다. 하지만 가능하다면 항상 작업이 어떻게 작동하는지 이해하는 것이 가장 좋습니다. 짧은 버전의 경우 꼭짓점의 3D 위치가 실제로 화면의 최종 2D 위치에 투영되는 방식입니다.

실제로 위 스니펫에서는 생략했습니다. Three.js가 셰이더 코드 자체의 맨 위에 추가하므로 걱정하지 않아도 됩니다. 실제로 빛 데이터, 꼭짓점 색상, 꼭짓점 노멀과 같이 그보다 더 많은 것이 추가됩니다. Three.js를 사용하지 않고 이 작업을 수행하려면 모든 유니폼과 속성을 직접 만들고 설정해야 합니다. 실제 사례

6. MeshShaderMaterial 사용

자, 셰이더를 설정했습니다. 하지만 Three.js에서 어떻게 사용할 수 있을까요? 그것은 엄청나게 쉬운 것입니다. 다음과 같습니다.

/**
* Assume we have jQuery to hand and pull out
* from the DOM the two snippets of text for
* each of our shaders
*/
var shaderMaterial = new THREE.MeshShaderMaterial({
vertexShader:   $('vertexshader').text(),
fragmentShader: $('fragmentshader').text()
});

여기서 Three.js는 해당 자료를 제공하는 메시에 연결된 셰이더를 컴파일하고 실행합니다. 정말 쉬워요. 아마 브라우저에서 3D로 실행된다는 의미이기 때문에 어느 정도 복잡성이 있을 것이라고 예상할 수 있습니다.

실제로 MeshShaderMaterial에 유니폼과 속성을 두 개 더 추가할 수 있습니다. 둘 다 벡터, 정수 또는 부동 소수점 수를 사용할 수 있지만 앞서 언급했듯이 유니폼은 전체 프레임, 즉 모든 꼭짓점에 동일하므로 단일 값인 경향이 있습니다. 그러나 속성은 꼭짓점별 변수이므로 배열이어야 합니다. 속성 배열의 값 수와 메시의 꼭짓점 수 사이에는 일대일 관계가 있어야 합니다.

7. 다음 단계

이제 애니메이션 루프, 꼭짓점 속성, 유니폼을 추가해 보겠습니다. 꼭짓점 셰이더가 일부 데이터를 프래그먼트 셰이더에 보낼 수 있도록 다양한 변수도 추가합니다. 결과적으로 분홍색이었던 구체는 상단에서 측면으로 빛을 낸 것처럼 보이고 깜빡이게 됩니다. 꽤 복잡하지만 세 가지 변수 유형은 물론 이러한 유형 간의 관계 및 기본 도형에 대해 잘 이해하는 데 도움이 되기를 바랍니다.

8. 모조 빛

평평한 색상 객체가 아니도록 색상을 업데이트해 보겠습니다. Three.js가 광원을 처리하는 방식을 살펴보겠습니다. 하지만 아마 지금 당장 필요한 것보다 훨씬 복잡하므로 모조로 만들어 보겠습니다. Three.js에 포함된 멋진 셰이더와 크리스 밀크와 Google Rome의 최근 놀라운 WebGL 프로젝트에 포함된 셰이더도 살펴봐야 합니다. 셰이더로 돌아갑니다. 프래그먼트 셰이더에 각 정규 꼭짓점을 제공하도록 Vertex 셰이더를 업데이트합니다. 이 작업은 아래와 같이 다양한 방법으로 수행할 수 있습니다.

// create a shared variable for the
// VS and FS containing the normal
varying vec3 vNormal;

void main() {

// set the vNormal value with
// the attribute value passed
// in by Three.js
vNormal = normal;

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(position,1.0);
}

프래그먼트 셰이더에서 동일한 변수 이름을 설정한 다음 구체의 위쪽에서 오른쪽으로 빛나는 빛을 나타내는 벡터와 함께 꼭짓점 노멀의 내적을 사용합니다. 결과적으로 3D 패키지의 방향 광원과 유사한 효과를 얻을 수 있습니다.

// same name and type as VS
varying vec3 vNormal;

void main() {

// calc the dot product and clamp
// 0 -> 1 rather than -1 -> 1
vec3 light = vec3(0.5,0.2,1.0);
    
// ensure it's normalized
light = normalize(light);

// calculate the dot product of
// the light to the vertex normal
float dProd = max(0.0, dot(vNormal, light));

// feed into our frag colour
gl_FragColor = vec4(dProd, dProd, dProd, 1.0);

}

내적이 작동하는 이유는 두 벡터가 주어지면 두 벡터가 얼마나 '유사'한지를 나타내는 숫자가 나오기 때문입니다. 정규화된 벡터가 정확히 같은 방향을 가리키는 경우 값 1을 얻습니다. 반대 방향을 가리키는 경우 -1이 표시됩니다. 이 숫자를 가져와서 조명에 적용합니다. 따라서 오른쪽 상단에 있는 꼭짓점의 값은 1에 가깝거나 같은 반면, 측면의 꼭짓점은 0에 가깝고 뒤쪽을 반올림하는 값은 -1이 됩니다. 음수의 경우 값을 0으로 고정하지만 숫자를 입력하면 결국 보이는 기본 조명이 됩니다.

다음 단계 꼭짓점 위치를 변경해 보는 것이 좋습니다.

9. 특성

이제 속성을 통해 각 꼭짓점에 랜덤 숫자를 연결하겠습니다. 이 숫자를 사용하여 꼭짓점을 정상에 따라 밀어냅니다. 최종 결과는 일종의 이상한 스파이크 공이 되어 페이지를 새로고침할 때마다 바뀝니다. 아직 애니메이션이 적용되지는 않지만 (이후에) 페이지 새로고침을 몇 번 하면 페이지가 무작위로 선택되어 표시됩니다.

먼저 꼭짓점 셰이더에 속성을 추가해 보겠습니다.

attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// push the displacement into the three
// slots of a 3D vector so it can be
// used in operations with other 3D
// vectors like positions and normals
vec3 newPosition = position + 
                    normal * 
                    vec3(displacement);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

어떤 모습인가요?

크게 다르지 않습니다. MeshShaderMaterial에서 속성이 설정되지 않았으므로 셰이더가 대신 0 값을 사용하기 때문입니다. 지금은 자리표시자와 같습니다. 잠시 후 JavaScript의 MeshShaderMaterial에 속성을 추가하면 Three.js가 자동으로 두 속성을 연결합니다.

또한 모든 속성과 마찬가지로 원래 속성은 읽기 전용이므로 업데이트된 위치를 vec3 변수에 할당해야 했습니다.

10. MeshShaderMaterial 업데이트

변위에 전원을 공급하는 데 필요한 속성으로 MeshShaderMaterial을 바로 업데이트해 보겠습니다. 참고: 속성은 꼭짓점당 값이므로 구의 꼭짓점당 값이 하나씩 필요합니다. 다음과 같습니다.

var attributes = {
displacement: {
    type: 'f', // a float
    value: [] // an empty array
}
};

// create the material and now
// include the attributes property
var shaderMaterial = new THREE.MeshShaderMaterial({
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

// now populate the array of attributes
var vertices = sphere.geometry.vertices;
var values = attributes.displacement.value
for(var v = 0; v < vertices.length; v++) {
values.push(Math.random() * 30);
}

이제 손상된 구를 볼 수 있습니다. 하지만 모든 변위가 GPU에서 발생하고 있다는 점입니다.

11. 빨판 애니메이션

애니메이션으로 만들어야 합니다. 어떻게 해야 할까요? 다음 두 가지를 마련해야 합니다.

  1. 각 프레임에 적용해야 하는 변위를 애니메이션으로 보여주는 유니폼입니다. 사인이나 코사인을 사용할 수 있습니다. 1에서 1까지 실행되기 때문입니다.
  2. JS의 애니메이션 루프

MeshShaderMaterial과 Vertex Shader 모두에 유니폼을 추가합니다 먼저 버텍스 셰이더:

uniform float amplitude;
attribute float displacement;
varying vec3 vNormal;

void main() {

vNormal = normal;

// multiply our displacement by the
// amplitude. The amp will get animated
// so we'll have animated displacement
vec3 newPosition = position + 
                    normal * 
                    vec3(displacement *
                        amplitude);

gl_Position = projectionMatrix *
                modelViewMatrix *
                vec4(newPosition,1.0);
}

다음으로, MeshShaderMaterial을 업데이트합니다.

// add a uniform for the amplitude
var uniforms = {
amplitude: {
    type: 'f', // a float
    value: 0
}
};

// create the final material
var shaderMaterial = new THREE.MeshShaderMaterial({
uniforms:       uniforms,
attributes:     attributes,
vertexShader:   $('#vertexshader').text(),
fragmentShader: $('#fragmentshader').text()
});

이제 셰이더가 완료되었습니다. 하지만 오른쪽으로 한 발짝 뒤로 물러난 것처럼 보입니다. 이는 주로 진폭 값이 0이고 이 값을 변위와 곱하기 때문에 아무 변화가 없기 때문입니다. 또한 애니메이션 루프를 설정하지 않았으므로 0이 다른 값으로 변경되는 것을 볼 수 없습니다.

이제 JavaScript에서 렌더링 호출을 함수로 래핑한 다음 requestAnimationFrame을 사용하여 호출해야 합니다. 여기에서 유니폼의 값도 업데이트해야 합니다.

var frame = 0;
function update() {

// update the amplitude based on
// the frame value
uniforms.amplitude.value = Math.sin(frame);
frame += 0.1;

renderer.render(scene, camera);

// set up the next call
requestAnimFrame(update);
}
requestAnimFrame(update);

12. 결론

이상입니다 이제 이 애니메이션은 이상하고 약간 깜빡이는 방식으로 깜빡이는 것을 확인할 수 있습니다.

셰이더에 관해 다룰 내용은 훨씬 더 많지만 이 소개가 도움이 되었기를 바랍니다. 이제 셰이더를 볼 때 이해할 수 있을 뿐만 아니라 자신만의 멋진 셰이더를 만들 수 있는 자신감을 가질 수 있습니다.