diff --git a/src/content/tutorials/en/intro-to-glsl.mdx b/src/content/tutorials/en/intro-to-glsl.mdx index ac268e7705..1df79b719c 100644 --- a/src/content/tutorials/en/intro-to-glsl.mdx +++ b/src/content/tutorials/en/intro-to-glsl.mdx @@ -39,7 +39,7 @@ The way to program the GPU in your browser is by using an API called [WebGL](htt ... function setup() { -  createCanvas(200, 200, WEBGL); + createCanvas(200, 200, WEBGL); } ... @@ -102,20 +102,23 @@ Each of these live in separate files and are loaded into p5.js using the `loadSh ```javascript let myShader; async function setup() { -  // load each shader file (don't worry, we will come back to these!) -  myShader = await loadShader('shader.vert', 'shader.frag'); -  // the canvas has to be created with WEBGL mode -  createCanvas(windowWidth, windowHeight, WEBGL); + // load each shader file + // (don't worry, we will come back to these!) + myShader = await loadShader('shader.vert', 'shader.frag'); + // the canvas has to be created with WEBGL mode + createCanvas(windowWidth, windowHeight, WEBGL); } function draw() { -  // shader() sets the active shader, which will be applied to what is drawn next -  shader(myShader); -  // apply the shader to a rectangle taking up the full canvas + // shader() sets the active shader, + // which will be applied to what is drawn next + shader(myShader); + // apply the shader to a rectangle + // taking up the full canvas plane(width, height); } ``` -There is also an additional function called `createShader()`, which can be used to load shaders directly from strings defined in your sketch.  +There is also an additional function called `createShader()`, which can be used to load shaders directly from strings defined in your sketch. ## Writing Shaders @@ -130,16 +133,18 @@ Shader files are written in the Graphics Library Shading Language, or GLSL (base For one, the shading language is much more strict about types. Each variable you create has to be labeled with the kind of data it is storing. Here is a list of some of the common types: ```glsl -vec2(x,y)     // a vector of two floats -vec3(x,y,z)   // a vector of three floats (could also be r,g,b) -vec4(x,y,z,w) // a vector of four floats (could also be r,g,b,a) -float         // a number with decimal points -int           // a whole number without decimal points -sampler2D     // a reference to a texture -mat2          // a 2x2 matrix -mat3          // a 3x3 matrix -mat4          // a 4x4 matrix -bool          // true or false +vec2(x,y) // a vector of two floats +vec3(x,y,z) // a vector of three floats + // (could also be r,g,b) +vec4(x,y,z,w) // a vector of four floats + // (could also be r,g,b,a) +float // a number with decimal points +int // a whole number without decimal points +sampler2D // a reference to a texture +mat2 // a 2x2 matrix +mat3 // a 3x3 matrix +mat4 // a 4x4 matrix +bool // true or false ``` In general shading languages are much more strict than JavaScript. A missing semicolon is not allowed and will result in an error message. You can't use different types of numbers, like floats or integers interchangeably. It also will complain about `float`s that aren't defined with a decimal place in them, so you will find yourself writing numbers like `0.0` or `1.0` often. @@ -208,7 +213,7 @@ Functions must declare the types of their parameters, and their return value. ```javascript function isBetween(val, start, end) { -  return val >= start && val <= end; + return val >= start && val <= end; } ``` @@ -218,7 +223,7 @@ function isBetween(val, start, end) { ```glsl bool isBetween(float val, float start, float end) { -  return val >= start && val <= end; + return val >= start && val <= end; } ``` @@ -271,10 +276,10 @@ Loops in GLSL have to stop at a constant value. If you want to end conditionally ```javascript let maxVal = 10; if (something) { -  maxVal = 20; + maxVal = 20; } for (let i = 0; i < maxVal; i++) { -  // do something + // do something } ``` @@ -285,13 +290,13 @@ for (let i = 0; i < maxVal; i++) { ```glsl int maxVal = 10; if (something) { -  maxVal = 20; + maxVal = 20; } for (int i = 0; i < 20; i++) { -  if (i == maxVal) { -    break; -  } -  // do something + if (i == maxVal) { + break; + } + // do something } ``` @@ -319,10 +324,13 @@ If you have a `vec4`, you can refer to its data like it is a color or a coordina //Each pair is equivalent: myVec.x myVec.r + myVec.y myVec.g + myVec.z myVec.b + myVec.w myVec.a ``` @@ -379,29 +387,31 @@ vec2 smallVec = bigVec.zy; Here's a simple vertex shader that applies the transformations and camera perspective supplied by p5.js: -`${begin('precision')} -precision highp float; -${end('precision')} -${begin('attributes')} -attribute vec3 aPosition; -${end('attributes')} -${begin('uniforms')} -// The transform of the object being drawn -uniform mat4 uModelViewMatrix; -// Transforms 3D coordinates to -// 2D screen coordinates -uniform mat4 uProjectionMatrix; -${end('uniforms')} -${begin('main')} -void main() { -  // Apply the camera transform -  vec4 viewModelPosition = - uModelViewMatrix * vec4(aPosition, 1.0); -  // Tell WebGL where the vertex goes -  gl_Position = - uProjectionMatrix * viewModelPosition;  -} -${end('main')}`}> +` + ${begin('precision')} + precision highp float; + ${end('precision')} + ${begin('attributes')} + attribute vec3 aPosition; + ${end('attributes')} + ${begin('uniforms')} + // The transform of the object being drawn + uniform mat4 uModelViewMatrix; + // Transforms 3D coordinates to + // 2D screen coordinates + uniform mat4 uProjectionMatrix; + ${end('uniforms')} + ${begin('main')} + void main() { + // Apply the camera transform + vec4 viewModelPosition = + uModelViewMatrix * vec4(aPosition, 1.0); + // Tell WebGL where the vertex goes + gl_Position = + uProjectionMatrix * viewModelPosition; + } + ${end('main')} +`}> The shader starts with a `precision` line. It can either be `lowp`, `mediump`, or `highp`. Using the highest quality is a good place to start to ensure your shaders look the same everywhere. On desktops and laptops, your GPU will likely use the highest quality regardless of what you write. On phones, using a lower quality might be faster, but it may make your shaders render differently. @@ -431,7 +441,7 @@ void main() { // Apply the camera transform vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); // Tell WebGL where the vertex goes - gl_Position = uProjectionMatrix * viewModelPosition;  + gl_Position = uProjectionMatrix * viewModelPosition; // Pass along data to the fragment shader vTexCoord = aTexCoord; vVertexColor = aVertexColor; @@ -443,15 +453,17 @@ void main() { The fragment shader is responsible for the color output of our shader and is where we will do a lot of our shader programming. Here is a very simple fragment shader that will just display the color red: -`${begin('precision')} -precision highp float; -${end('precision')} -${begin('main')} -void main() { -  vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0); -  gl_FragColor = myColor; -} -${end('main')}`}> +` + ${begin('precision')} + precision highp float; + ${end('precision')} + ${begin('main')} + void main() { + vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0); + gl_FragColor = myColor; + } + ${end('main')} +`}> The fragment shader begins again with a line specifying the float `precision`. This should match the `precision` in your vertex shader. @@ -471,129 +483,129 @@ Now that we have a vertex shader and a fragment shader, these can be saved to se A simple shader like this can be useful by itself, but there are times when it's necessary to communicate variables from the p5.js sketch to a shader. This is when uniforms come in. Uniforms are a type of variable that can be sent from a sketch to a shader. These make it possible to have much more control over a shader from JavaScript. -Uniforms are defined at the top of the file, outside of `main()`. You can access them in both your vertex and your fragment shader. In the example below, the value returned by the p5.js method `millis()` is passed to a 'time' uniform to introduce motion in the vertex shader. +Uniforms are defined at the top of the file, outside of `main()`. You can access them in both your vertex and your fragment shader. In the example below, the value returned by the p5.js method `millis()` is passed to a `time` uniform to introduce motion in the vertex shader. It also works in the fragment shader. In the following example, we create a color uniform, `myColor`, that will allow us to change the color from the JavaScript part of our sketch. Just remember that in shaders, color channel values go from 0-1 instead of 0-255. You can see the complete list of uniforms that p5.js provides for you in the [p5.js WebGL Mode Architecture](https://github.com/processing/p5.js/blob/main/contributor_docs/webgl_mode_architecture.md) document. @@ -606,30 +618,31 @@ You can see the complete list of uniforms that p5.js provides for you in the [p5 For example, you may want to use a shape's texture coordinates in the fragment shader. These come in the form of a `vec2`, where coordinates go between 0 and 1. This initially comes in an `attribute` from p5.js, and those are only accessible in the vertex shader. Let's look at what our standard vertex shader does to pass that to fragment shaders: -`precision highp float; - -attribute vec3 aPosition; -${begin('texcoord')} -attribute vec2 aTexCoord; -${end('texcoord')} -attribute vec4 aVertexColor; - -uniform mat4 uModelViewMatrix; -uniform mat4 uProjectionMatrix; - -${begin('varying')} -varying vec2 vTexCoord; -${end('varying')} -varying vec4 vVertexColor; -void main() { -  // Apply the camera transform -  vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); -  // Tell WebGL where the vertex goes -  gl_Position = uProjectionMatrix * viewModelPosition; -${begin('assign')} -  vVertexColor = aVertexColor; -${end('assign')} -} +` + precision highp float; + + attribute vec3 aPosition; + ${begin('texcoord')} + attribute vec2 aTexCoord; + ${end('texcoord')} + attribute vec4 aVertexColor; + + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; + + ${begin('varying')} + varying vec2 vTexCoord; + ${end('varying')} + varying vec4 vVertexColor; + void main() { + // Apply the camera transform + vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); + // Tell WebGL where the vertex goes + gl_Position = uProjectionMatrix * viewModelPosition; + ${begin('assign')} + vVertexColor = aVertexColor; + ${end('assign')} + } `}> The texture coordinates initially come in the form of an `attribute` called `aTexCoord`. This is automatically filled in by p5.js. @@ -642,17 +655,19 @@ ${end('assign')} -Since we defined a `varying` called `vTexCoord` in the vertex shader, we can now use it in the fragment shader as well. Here is a simple fragment shader that maps the x value to the red channel and the y value to the blue channel. Note that while `vTexCoord` is defined *per vertex* in the vertex shader, its value is defined *per pixel* in the fragment shader. To get the per-pixel value, WebGL smoothly interpolates between the per-vertex values on each face. +Since we defined a `varying` called `vTexCoord` in the vertex shader, we can now use it in the fragment shader as well. Here is a simple fragment shader that maps the x value to the red channel and the y value to the green channel. Note that while `vTexCoord` is defined *per vertex* in the vertex shader, its value is defined *per pixel* in the fragment shader. To get the per-pixel value, WebGL smoothly interpolates between the per-vertex values on each face. -`${begin('shader')} -precision highp float; -varying vec2 vTexCoord; -void main() { -  // Assign the coordinates to the color output of the shader -  gl_FragColor = vec4(vTexCoord.x, vTexCoord.y, 1.0, 1.0); -} -${end('shader')}`}> +` + ${begin('shader')} + precision highp float; + varying vec2 vTexCoord; + void main() { + // Assign the coordinates to the color output of the shader + gl_FragColor = vec4(vTexCoord.x, vTexCoord.y, 1.0, 1.0); + } + ${end('shader')} +`}> The result of using this shader on a `plane(width, height)`: @@ -677,97 +692,97 @@ There are a number of `uniform`s that will be available to you in a filter shade Combining these, `texture2D(tex0, vTexCoord)` returns the color of the current pixel on the canvas, which you can then modify. In this example, we create a custom black-and-white filter by replacing the red and green channels with the blue channel: Another thing you might want to try is modifying the *input* to `texture2D` rather than modifying its output. Adjusting the texture coordinate used can create an offset from the original or a warp effect if the offset is different per pixel: ## Conclusion @@ -782,7 +797,7 @@ Want to keep learning more about shaders? Check out some of these websites! - [p5js Shader Examples](https://github.com/aferriss/p5jsShaderExamples), a collection of resources by Adam Ferriss. - [OpenGL ES 2.0 Specification](https://registry.khronos.org/OpenGL/specs/es/2.0/es_cm_spec_2.0.pdf), super technical specification for GLSL - [WebGL Quick Reference card](https://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf), another slightly dense technical reference, but it contains lots of goodies about GLSL functions -- [Shaderific GLSL ES reference](https://shaderific.com/glsl.html), a somewhat slimmed-down reference for built-in GLSL functions and data types  +- [Shaderific GLSL ES reference](https://shaderific.com/glsl.html), a somewhat slimmed-down reference for built-in GLSL functions and data types ## Glossary diff --git a/src/content/tutorials/ko/intro-to-glsl.mdx b/src/content/tutorials/ko/intro-to-glsl.mdx new file mode 100644 index 0000000000..73b3bed8c0 --- /dev/null +++ b/src/content/tutorials/ko/intro-to-glsl.mdx @@ -0,0 +1,863 @@ +--- +title: "GLSL 입문" +description: GLSL을 사용해 컴퓨터의 GPU로 흥미로운 시각 효과를 만드는 여러 방법을 소개합니다. +category: webgl +categoryIndex: 3 +featuredImage: ../images/featured/intro-to-shaders.jpg +featuredImageAlt: 뒤틀리고 물결치는 도시 거리 +relatedContent: + examples: + - en/11_3d/04_filter_shader/description + - en/11_3d/05_adjusting_positions_with_a_shader/description + - en/11_3d/06_framebuffer_blur/description + references: + - en/p5/createfiltershader + - en/p5/loadshader + - en/p5/p5shader +authors: + - Dave Pagurek + - Austin Lee Slominski + - Adam Ferriss +--- + +import EditableSketch from "../../../components/EditableSketch/index.astro"; +import Callout from "../../../components/Callout/index.astro"; +import AnnotatedCode from "../../../components/AnnotatedCode/index.astro"; +import { Image } from "astro:assets"; +import uv from "../images/webgl/uv_example.png"; + +현대의 컴퓨터에는 GPU(Graphics Processing Unit)라는 특별한 하드웨어가 들어 있습니다. 셰이더는 이 GPU에서 실행되는 특수한 프로그램으로, 놀라운 것들을 해낼 수 있습니다. 셰이더는 GPU를 활용해 많은 픽셀을 한 번에 병렬로 처리하므로 매우 빠르며, 노이즈 생성, 블러 같은 필터 적용, 다각형에 음영 입히기처럼 컴퓨터 그래픽스 작업에 특히 잘 맞습니다. + +셰이더 프로그래밍은 처음에는 낯설고 어렵게 느껴질 수 있습니다. p5.js의 2D 드로잉과는 다른 방식으로 접근해야 하기 때문입니다. 이 튜토리얼에서는 셰이더 프로그래밍의 기초를 짚어 보고, 더 살펴볼 만한 자료도 함께 소개합니다. + + +## 준비 + +브라우저에서 GPU를 프로그래밍하는 데 [WebGL](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API)이라는 API를 사용합니다. p5.js는 셰이더 작업에 매우 유용합니다. WebGL의 반복적인 준비 과정을 대신 처리해 주기 때문에, 셰이더 코드에 집중할 수 있습니다. 셰이더를 사용해보려면 먼저 p5.js 캔버스를 WebGL 모드로 설정해야 합니다. 이를 위해 `createCanvas()`의 세 번째 매개변수로 `WEBGL` 상수를 추가합니다. + +```javascript +... + +function setup() { + createCanvas(200, 200, WEBGL); +} + +... +``` + +셰이더 프로그램은 두 부분으로 이루어집니다. 바로 **정점 셰이더**(vertex shader)와 **프래그먼트 셰이더**(fragment shader)입니다. 정점 셰이더는 도형의 각 정점마다 한 번씩 실행되며, 그 정점이 화면 어디에 그려질지를 결정합니다. 프래그먼트 셰이더는 도형의 모든 픽셀마다 한 번씩 실행되며, 각 픽셀의 색을 결정합니다. + + + + + + + + + + + + + + + + + + + + + + + +
+ +![빨간 구](../images/webgl/sphere.png) + + + +![시간에 따라 흔들리고 뒤틀리는 빨간 구](../images/webgl/vertshader.gif) + + + +![울퉁불퉁한 구의 실루엣 안이 빨강과 파랑 줄무늬로 채워진 모습](../images/webgl/fragshader.gif) + +
+ +*원래 형태* + + + +*커스텀 정점 셰이더는 도형 안의 정점 위치를 조정할 수 있습니다* + + + +*커스텀 프래그먼트 셰이더는 도형 내부의 색을 조정할 수 있습니다* + +
+ +{/* Note for contributors: Images/gifs generated from https://editor.p5js.org/davepagurek/sketches/gs-DbLzqV */} + +이 둘은 각각 별도의 파일에 들어 있으며, `loadShader()` 함수로 p5.js로 불러와 `setup()`, `draw()` 에서 사용할 수 있습니다. 다음 예제를 통해 p5.js에서 기본 셰이더를 설정하는 방법을 살펴봅시다. + +```javascript +let myShader; +async function setup() { + // 각 셰이더 파일을 불러옵니다 + // (걱정 마세요, 이건 곧 다시 살펴볼 거예요!) + myShader = await loadShader('shader.vert', 'shader.frag'); + // 캔버스는 WEBGL 모드로 만들어야 합니다 + createCanvas(windowWidth, windowHeight, WEBGL); +} +function draw() { + // shader()는 활성 셰이더를 설정하고, + // 이후 그려지는 것들에 적용됩니다 + shader(myShader); + // 캔버스 전체를 덮는 사각형에 셰이더를 적용합니다 + plane(width, height); +} +``` + +추가로 `createShader()`라는 함수도 있는데, 이 함수는 스케치 안에 문자열로 직접 작성한 셰이더를 불러올 때 사용할 수 있습니다.  + + +## 셰이더 작성하기 + +이제 `loadShader()`에서 참조한 정점, 프래그먼트 셰이더 파일 안에 무엇이 들어가는지 살펴보겠습니다. + + +### 셰이딩 언어(GLSL) + +셰이더 파일은 Graphics Library Shading Language, 즉 GLSL(OpenGL 2.0, GLSL ES 1.00 기반)로 작성합니다. 문법과 구조가 우리가 익숙한 자바스크립트와는 꽤 다릅니다. GLSL은 C와 비슷한 문법을 가지므로, 자바스크립트에는 없는 개념들도 함께 등장합니다. + +우선 셰이딩 언어는 타입에 훨씬 엄격합니다. 만드는 모든 변수에는 어떤 데이터를 저장하는지 타입을 명시해야 합니다. 아래는 자주 쓰이는 타입 몇 가지입니다. + +```glsl +vec2(x,y) // 두 개의 float로 이루어진 벡터 +vec3(x,y,z) // 세 개의 float로 이루어진 벡터 + // (r,g,b로도 볼 수 있음) +vec4(x,y,z,w) // 네 개의 float로 이루어진 벡터 + // (r,g,b,a로도 볼 수 있음) +float // 소수점을 가지는 숫자 +int // 소수점이 없는 정수 +sampler2D // 텍스처를 참조하는 값 +mat2 // 2x2 행렬 +mat3 // 3x3 행렬 +mat4 // 4x4 행렬 +bool // true 또는 false +``` + +전반적으로 셰이딩 언어는 자바스크립트보다 훨씬 엄격합니다. 세미콜론 하나만 빠져도 오류가 발생합니다. float와 int 같은 서로 다른 숫자 타입을 섞어서 쓸 수도 없습니다. 또 `float` 값은 반드시 소수점까지 써 주어야 하므로, `0.0`이나 `1.0` 같은 숫자를 자주 쓰게 됩니다. + +다음은 GLSL에서 달라지는 몇 가지 예시입니다. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +Javascript + + + +GLSL + +
+ +모든 변수에 타입이 필요합니다. + + + +```javascript +let a = 1; +let b = 0.5; +``` + + + +```glsl +int a = 1; +float b = 0.5; +``` + +
+ +함수는 매개변수의 타입과 반환값의 타입을 모두 선언해야 합니다. + + + +```javascript +function isBetween(val, start, end) { +  return val >= start && val <= end; +} +``` + + + +```glsl +bool isBetween(float val, float start, float end) { +  return val >= start && val <= end; +} +``` + +
+ +int와 float 사이의 변환은 직접 해 주어야 합니다. + + + +```javascript +let a = 1; +let b = 0.5; +let c = b + 2; +let d = a + b; +``` + + + +```glsl +int a = 1; +float b = 0.5; +float c = b + 2.0; +float d = float(a) + b; +``` + +
+ +GLSL의 반복문은 상수 값에서 멈춰야 합니다. 조건에 따라 중간에 끝내고 싶다면 `break`를 사용할 수 있습니다. + + + +```javascript +let maxVal = 10; +if (something) { +  maxVal = 20; +} +for (let i = 0; i < maxVal; i++) { +  // 무언가를 합니다 +} +``` + + + +```glsl +int maxVal = 10; +if (something) { +  maxVal = 20; +} +for (int i = 0; i < 20; i++) { +  if (i == maxVal) { +    break; +  } +  // 무언가를 합니다 +} +``` + +
+ +제약이 많기는 하지만, 어떤 면에서는 GLSL이 더 편하게 느껴질 수도 있습니다! 특히, 벡터를 다루는 데 유용한 여러 축약 표현이 있습니다. + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +`vec4`가 있다면, 그 데이터를 색처럼 읽을 수도 있고 좌표처럼 읽을 수도 있습니다. 둘은 완전히 같으니, 코드가 더 읽기 쉬운 쪽을 골라 사용하면 됩니다. + + + +```glsl +// 각 쌍은 서로 같습니다: +myVec.x +myVec.r + +myVec.y +myVec.g + +myVec.z +myVec.b + +myVec.w +myVec.a +``` + +
+ +모든 값이 같은 벡터를 만들고 싶을 때는 같은 값을 여러 번 반복해서 쓰지 않아도 됩니다. 한 번만 써도 충분합니다. + + + +```glsl +// 두 표현은 같습니다 +myVec = vec3(0.5, 0.5, 0.5); +myVec = vec3(0.5); +``` + +
+ +더 큰 벡터에서 더 작은 벡터를 꺼낼 수도 있습니다. 이를 "스위즐링(swizzling)"이라고 하며, `.` 뒤에 원하는 순서대로 여러 속성을 이어 붙여 새 벡터를 만듭니다. + + + +```glsl +vec4 bigVec = vec4(1.0, 2.0, 3.0, 4.0); +// vec2(bigVec.z, bigVec.y)와 같습니다 +vec2 smallVec = bigVec.zy; +``` + +
+ + +### 정점 셰이더 + +다음은 간단한 정점 셰이더 예시입니다. p5.js에서 제공하는 변환과 카메라 원근을 적용합니다. + + +`${begin('precision')} + precision highp float; +${end('precision')} +${begin('attributes')} + attribute vec3 aPosition; +${end('attributes')} +${begin('uniforms')} + // 그려지는 객체의 변환 + uniform mat4 uModelViewMatrix; + // 3D 좌표를 2D 화면 좌표로 변환합니다 + uniform mat4 uProjectionMatrix; +${end('uniforms')} +${begin('main')} + void main() { + // 카메라 변환을 적용합니다 + vec4 viewModelPosition = + uModelViewMatrix * vec4(aPosition, 1.0); + // 이 정점이 어디에 그려질지 WebGL에 알려 줍니다 + gl_Position = + uProjectionMatrix * viewModelPosition; + } +${end('main')}`}> + + 셰이더는 `precision` 줄로 시작합니다. 여기에는 `lowp`, `mediump`, `highp` 중 하나를 씁니다. 처음에는 가장 높은 품질을 선택하는 것이 좋습니다. 그래야 다양한 환경에서 비슷하게 보입니다. 데스크톱이나 노트북은 어떤 값을 써도 보통 최고 품질로 처리됩니다. 휴대폰에서는 더 낮은 품질을 사용하는 것이 빠를 수 있지만, 렌더링 결과가 달라질 수 있습니다. + + + 셰이더의 *attribute*는 각 정점마다 달라지는 값을 담고 있으며, p5.js는 이를 통해 각 정점의 위치 같은 정보를 셰이더에 전달합니다. 이 셰이더에서 attribute는 `vec3` 타입으로, x, y, z 값을 담고 있습니다. Attribute는 정점 셰이더에서만 사용하는 특별한 변수 타입이며, 보통 p5.js가 제공합니다. `rect()`나 `vertex()` 같은 p5.js 함수를 사용할 때 p5.js가 정점 정보를 자동으로 셰이더에 넘겨 줍니다. + + + 셰이더의 *uniform*은 그려지는 도형 전체에서 일정한 값을 뜻합니다. 이 셰이더에서는 각 uniform이 `mat4` 타입인데, 이는 이동, 크기 조절, 회전 같은 변환을 표현할 때 자주 쓰이는 타입입니다. 점에 `mat4`를 곱하면 그 행렬이 나타내는 변환이 적용됩니다. 이 셰이더의 uniform들은 p5.js가 자동으로 제공하지만, 뒤에서 직접 커스텀 uniform을 전달하는 방법도 보게 될 것입니다. 참고로 행렬 곱셈에서는 순서가 중요합니다. 대부분의 경우 행렬을 먼저 쓰고, 그 뒤에 곱할 값을 씁니다. + + + 모든 정점 셰이더에는 `main()` 함수가 필요합니다. 이 안에서 `gl_Position`에 값을 할당해 정점의 위치를 정합니다. 이 값은 *클립 공간*(clip space)에 있으며, x, y, z 값은 한쪽 끝에서 다른 쪽 끝으로 갈 때 -1에서 1까지의 범위를 가집니다. `uProjectionMatrix`로 3D 점을 곱하면 p5.js의 카메라 설정을 이용해 이 변환을 자동으로 해 줍니다. 그 전에 이 셰이더는 `uModelViewMatrix`도 곱해 주는데, 이는 도형을 그리기 전에 설정된 누적 변환을 적용하기 위해서입니다. + + + +아직 이 내용이 완전히 이해되지 않아도 괜찮습니다. 정점 셰이더는 중요한 역할을 하지만, 대개 프래그먼트 셰이더에서 만든 결과가 도형 위에 올바르게 보이도록 하는 것이 전부입니다. 아마 여러 프로젝트에서 같은 정점 셰이더를 반복해서 사용하게 될 것입니다. 다음은 정점별 색과 텍스처 좌표 등 정보를 처리하는 표준 정점 셰이더입니다. + +```glsl +precision highp float; +attribute vec3 aPosition; +attribute vec2 aTexCoord; +attribute vec4 aVertexColor; +uniform mat4 uModelViewMatrix; +uniform mat4 uProjectionMatrix; +varying vec2 vTexCoord; +varying vec4 vVertexColor; +void main() { + // 카메라 변환을 적용합니다 + vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); + // 이 정점이 어디에 그려질지 WebGL에 알려 줍니다 + gl_Position = uProjectionMatrix * viewModelPosition;  + // 데이터를 프래그먼트 셰이더로 전달합니다 + vTexCoord = aTexCoord; + vVertexColor = aVertexColor; +} +``` + +### 프래그먼트 셰이더 + +프래그먼트 셰이더는 셰이더의 색 출력 결과를 담당합니다. 셰이더 프로그래밍의 많은 부분이 여기서 이루어집니다. 다음은 단순히 빨간색을 표시하는 간단한 프래그먼트 셰이더입니다. + + +`${begin('precision')} + precision highp float; +${end('precision')} +${begin('main')} + void main() { + vec4 myColor = vec4(1.0, 0.0, 0.0, 1.0); + gl_FragColor = myColor; + } +${end('main')}`}> + + 프래그먼트 셰이더도 float의 `precision`을 지정하는 줄로 시작합니다. 이 값은 정점 셰이더의 `precision`과 동일해야 합니다. + + + 정점 셰이더와 마찬가지로 프래그먼트 셰이더에도 `main()` 함수가 필요합니다. 다만 여기서는 `gl_Position`을 설정하는 대신, GLSL이 제공하는 특별한 변수 `gl_FragColor`에 색을 할당합니다. + + `myColor` 변수는 `vec4`로 정의되어 네 개의 값을 저장합니다. 지금은 색을 다루고 있으므로, 빨강, 초록, 파랑, 알파입니다. 셰이더는 기본 p5.js 스케치처럼 0~255 범위의 색을 쓰지 않습니다. 대신 0.0에서 1.0 사이의 값을 사용합니다. + + + +이제 정점 셰이더와 프래그먼트 셰이더를 모두 갖추었으니, 이 코드를 각각 별도의 파일(`shader.vert`, `shader.frag`)로 저장해 `loadShader()`로 스케치에 불러올 수 있습니다. + + +## 유니폼: 스케치에서 셰이더로 데이터 전달하기 + +이처럼 단순한 셰이더도 충분히 유용할 수 있지만, p5.js 스케치의 변수를 셰이더와 주고받아야 할 경우도 있습니다. 이럴 때 사용하는 것이 유니폼입니다. 유니폼은 스케치에서 셰이더로 보낼 수 있는 변수의 한 종류입니다. 유니폼을 사용하여 자바스크립트에서 셰이더를 훨씬 더 세밀하게 제어할 수 있습니다. + +유니폼은 파일의 맨 위, `main()` 바깥에서 정의합니다. 이는 정점 셰이더와 프래그먼트 셰이더 양쪽 모두에서 접근할 수 있습니다. 아래 예제에서는 p5.js의 `millis()` 메서드가 반환하는 값을 `time`이라는 유니폼으로 전달해, 정점 셰이더에 움직임을 더합니다. + + + +프래그먼트 셰이더에서도 유니폼을 똑같이 사용할 수 있습니다. 다음 예제에서는 `myColor`라는 색 유니폼을 만들어 자바스크립트 쪽에서 색을 바꿀 수 있게 합니다. 다만 셰이더에서는 색 채널 값이 0\~255가 아니라 0\~1 범위라는 점을 기억해 두세요. + + + 카메라 공간) + uniform mat4 uModelViewMatrix; + + // 3D 좌표를 2D 화면 좌표로 변환합니다 + uniform mat4 uProjectionMatrix; + + void main() { + // 카메라 변환을 적용합니다 + vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); + + // 이 정점이 어디에 그려질지 WebGL에 알려 줍니다 + gl_Position = uProjectionMatrix * viewModelPosition; + } + \`; + + let frag = \` + precision highp float; + + // 색을 제어하기 위한 커스텀 유니폼 + uniform vec4 myColor; + + void main() { + gl_FragColor = myColor; + } + \` + + function setup() { + createCanvas(200, 200, WEBGL); + myShader = createShader(vert, frag); + } + + function draw() { + background(255); + noStroke(); + + // 우리가 만든 셰이더를 사용합니다 + shader(myShader); + + // 마우스 x 위치로 빨강, y 위치로 초록 색을 만들고 + // 셰이더로 전달합니다 + myShader.setUniform('myColor', [ + map(mouseX, 0, width, 0, 1, true), // 빨강 + map(mouseY, 0, width, 0, 1, true), // 초록 + 0, // 파랑 + 1 // 알파 + ]); + + // 셰이더를 사용해 도형을 그립니다 + circle(0, 0, 100); + } +`} /> + +p5.js가 제공하는 유니폼의 전체 목록은 [p5.js WebGL Mode Architecture](https://github.com/processing/p5.js/blob/main/contributor_docs/webgl_mode_architecture.md) 문서에서 확인할 수 있습니다. + + +## Varyings: 정점 셰이더에서 프래그먼트 셰이더로 데이터 전달하기 + +*Varying* 변수는 정점 셰이더와 프래그먼트 셰이더 사이에서 데이터를 공유합니다. 덕분에 위치나 다른 기하 정보도 프래그먼트 셰이더 안에서 활용할 수 있습니다. + +예를 들어 프래그먼트 셰이더에서 도형의 텍스처 좌표를 사용하고 싶을 수 있습니다. 이 값은 `vec2` 형태로 들어오며, 좌표 범위는 0에서 1 사이입니다. 처음에는 p5.js가 제공하는 `attribute`로 들어오고, `attribute`는 정점 셰이더에서만 접근할 수 있습니다. 그럼 표준 정점 셰이더가 이 정보를 프래그먼트 셰이더로 어떻게 전달하는지 보겠습니다. + + + `precision highp float; + + attribute vec3 aPosition; + ${begin('texcoord')} + attribute vec2 aTexCoord; + ${end('texcoord')} + attribute vec4 aVertexColor; + + uniform mat4 uModelViewMatrix; + uniform mat4 uProjectionMatrix; + + ${begin('varying')} + varying vec2 vTexCoord; + ${end('varying')} + varying vec4 vVertexColor; + void main() { + // 카메라 변환을 적용합니다 + vec4 viewModelPosition = uModelViewMatrix * vec4(aPosition, 1.0); + // 이 정점이 어디에 그려질지 WebGL에 알려 줍니다 + gl_Position = uProjectionMatrix * viewModelPosition; + ${begin('assign')} + vVertexColor = aVertexColor; + ${end('assign')} + } +`}> + + 텍스처 좌표는 처음에 `aTexCoord`라는 `attribute` 형태로 들어옵니다. 이 값은 p5.js가 자동으로 채워 줍니다. + + + 여기서는 `varying` 변수를 선언합니다. 정점 셰이더에서 선언한 varying은 프래그먼트 셰이더에서도 다시 선언할 수 있습니다. 이를 통해 정점 셰이더가 할당한 값을 프래그먼트 셰이더에서 사용할 수 있습니다. + + + *attribute*의 값을 *varying* 변수에 할당함으로써, 프래그먼트 셰이더가 읽을 수 있는 자리로 데이터를 복사합니다. + + + +정점 셰이더에서 `vTexCoord`라는 varying을 정의했으므로, 프래그먼트 셰이더에서도 이를 사용할 수 있습니다. 아래의 단순한 프래그먼트 셰이더는 x 값을 빨간 채널에, y 값을 초록 채널에 대응시킵니다. `vTexCoord`는 정점 셰이더에서는 *각 정점별* 값이지만, 프래그먼트 셰이더에서는 *각 픽셀별* 값이라는 점에 주목하세요. 픽셀별 값을 얻기 위해 WebGL은 각 면의 꼭짓점 값 사이를 부드럽게 보간합니다. + + +` + ${begin('shader')} + precision highp float; + varying vec2 vTexCoord; + void main() { + // 좌표를 셰이더의 색 출력으로 할당합니다 + gl_FragColor = vec4(vTexCoord.x, vTexCoord.y, 1.0, 1.0); + } + ${end('shader')} +`}> + + + 이 셰이더를 `plane(width, height)`에 적용하면 다음과 같은 결과가 나옵니다. + + 왼쪽 위는 검정, 오른쪽 위는 마젠타, 오른쪽 아래는 흰색, 왼쪽 아래는 시안으로 보이는 직사각형 그라데이션. + + + + + +## 필터 셰이더 + +p5.js에서 필터란 캔버스의 모든 픽셀을 살펴본 뒤 그것들을 다른 값으로 바꾸는 것입니다. 색을 반전하거나 캔버스에 블러를 적용하는 등 다양한 내장 필터가 있습니다. 직접 프래그먼트 셰이더를 작성해 자신만의 필터를 만들 수도 있습니다. + +필터 셰이더에는 프래그먼트 셰이더만 있으면 됩니다. 정점 셰이더는 주로 도형의 위치를 정하는 역할인데, 필터는 늘 캔버스 전체에 적용되기 때문입니다. 그래서 p5.js가 필터 셰이더에 기본 정점 셰이더를 제공해 줍니다. `loadShader` 대신 `createFilterShader(src)`를 사용하고, 셰이더 소스 코드를 담은 문자열을 넣으면 됩니다. + +필터 셰이더에서는 사용할 수 있는 `uniform`들이 몇 가지 있으며, 자세한 내용은 [`createFilterShader` 문서](https://p5js.org/reference/p5/createFilterShader)에서 확인할 수 있습니다. 시작할 때 특히 알아 두면 좋은 두 가지는 다음과 같습니다. + +- `uniform sampler2D tex0`는 캔버스의 내용을 담고 있는 텍스처입니다. +- `varying vec2 vTexCoord`는 현재 픽셀의 캔버스 좌표를 담고 있으며, 범위는 0에서 1입니다. + +이 둘을 조합하여 `texture2D(tex0, vTexCoord)`로 현재 픽셀의 색을 반환하고, 그 색을 원하는 대로 바꿀 수 있습니다. 아래 예제에서는 파란 채널 값을 빨강과 초록 채널로 복사해, 사용자 정의 흑백 필터를 만듭니다. + + + +또 다른 시도는 `texture2D`의 *출력*을 바꾸는 대신 *입력*을 바꾸는 것입니다. 사용하는 텍스처 좌표를 조정하면 원본에서 살짝 밀린 효과를 만들 수도 있고, 픽셀마다 오프셋을 다르게 주면 뒤틀림(warp) 효과도 만들 수 있습니다. + + + +## 마무리 + +여기까지의 내용만으로도 기본적인 셰이더를 만들 수 있습니다. 하지만 셰이더 프로그래밍의 세계는 훨씬 더 깊고 넓으며, 이 튜토리얼에서 다루지 못한 다양한 주제와 기법이 존재합니다. p5.js의 셰이더는 시각 효과, 텍스처, 3D 기하 구조 위에 입힐 수 있는 다양한 표현을 만드는 데 강력한 도구가 됩니다. + +셰이더를 더 배우고 싶다면 아래 자료도 살펴보세요! + +- [The Book of Shaders](https://thebookofshaders.com/), Patricio Gonzalez Vivo와 Jen Lowe의 셰이더 가이드 +- [p5.js shaders](https://itp-xstory.github.io/p5js-shaders/#/), Casey Conchinha와 Louise Lessél의 셰이더 가이드 +- [Shadertoy](https://www.shadertoy.com/), 브라우저 편집기에서 작성된 셰이더를 모아 둔 방대한 온라인 컬렉션 +- [p5js Shader Examples](https://github.com/aferriss/p5jsShaderExamples), Adam Ferriss가 모아 둔 자료 모음 +- [OpenGL ES 2.0 Specification](https://registry.khronos.org/OpenGL/specs/es/2.0/es_cm_spec_2.0.pdf), GLSL에 대한 매우 기술적인 명세 문서 +- [WebGL Quick Reference card](https://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf), 다소 빽빽하지만 GLSL 함수에 관한 유용한 정보가 많은 참고 카드 +- [Shaderific GLSL ES reference](https://shaderific.com/glsl.html), GLSL 내장 함수와 데이터 타입을 조금 더 간결하게 정리한 레퍼런스  + + +## 용어집 + +#### 셰이더 + +다양한 시각 효과와 필터를 효율적으로 만들어 낼 수 있는 GPU 프로그램입니다. + + +#### GLSL + +Graphics Library Shader Language(GLSL)는 셰이더를 작성할 때 사용하는 프로그래밍 언어입니다. + + +#### 유니폼 (Uniform) + +스케치에서 셰이더로 전달되는 변수입니다. + + +#### Varying + +정점 셰이더에서 프래그먼트 셰이더로 전달되는 변수입니다. + + +#### 벡터 (`vec2` / `vec3` / `vec4`) + +흔히 두 개, 세 개, 또는 네 개의 숫자를 묶어서 저장하는 데이터 타입으로, 색, 위치 등을 표현하는 데 사용됩니다. + + +#### Float + +소수점을 가질 수 있는 부동소수점 숫자를 저장하는 데이터 타입입니다. + + +#### Int + +소수점이 없는 정수를 저장하는 데이터 타입입니다. + + +#### Sampler + +셰이더로 전달되는 텍스처를 나타내는 데이터 타입입니다. GLSL에서는 보통 `sampler2D`로 표현합니다. + + +#### Attribute + +p5.js 스케치에서 생성되어 정점 셰이더에서 사용할 수 있게 되는 GLSL 변수입니다. 대부분의 경우 p5.js가 이를 제공합니다. + + +#### 텍스처 + +셰이더 프로그램에 전달되는 이미지입니다. `texture2D()` 함수를 사용해 샘플링할 수 있습니다. + + +#### 타입 + +int, float, vector 등 데이터의 형식을 설명하는 라벨입니다. + + +#### 정점 셰이더 (Vertex Shader) + +3D 공간에서 기하학적 객체의 위치를 정하는 셰이더 프로그램의 부분입니다. + + +#### 프래그먼트 셰이더 (Fragment Shader) + +셰이더가 출력하는 각 픽셀의 색과 외형을 담당하는 셰이더 프로그램의 부분입니다.