GLSL 퀵 레퍼런스

어떤 지점(p)에서 Ray(r)에 가장 가까운 r 위의 좌표 얻기

struct ray {
  vec3 o, d;
};

vec3 ClosestPoint(ray r, vec3 p) {
  return r.o + max(0., dot(p-r.o, r.d)) * r.d;
}

위의 ClosePoint를 이용하면 ray와 어떤 지점에 대한 최단 거리를 아래 함수를 이용해 구할 수 있다.

float DistRay(ray r, vec3 p) {
  return length(p - ClosestPoint(r, p));
}

선 그리기 함수

float line(vec2 p, vec2 a, vec2 b) {
  vec2 pa = p - a, ba = b - a;
  float t = clamp(dot(pa, ba)/dot(ba, ba), 0., 1.);
  vec2 c = a + ba * t;
  float d = length(c - p);
  return smoothstep(fwidth(d), 0., d - .001);
}

0 ~ 1 사이의 값을 갖는 간단한 노이즈 함수

float random (in vec2 st) {
    return fract(sin(dot(st.xy,
        vec2(12.9898,78.233)))*
        43758.5453123);
}

// Based on Morgan McGuire @morgan3d
// https://www.shadertoy.com/view/4dS3Wd
float noise (in vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);

    // Four corners in 2D of a tile
    float a = random(i);
    float b = random(i + vec2(1.0, 0.0));
    float c = random(i + vec2(0.0, 1.0));
    float d = random(i + vec2(1.0, 1.0));

    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(a, b, u.x) +
            (c - a)* u.y * (1.0 - u.x) +
            (d - b) * u.x * u.y;
}

-1 ~ 1 사이의 값을 갖는 Simplex 노이즈 함수

// Some useful functions
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

//
// Description : GLSL 2D simplex noise function
//      Author : Ian McEwan, Ashima Arts
//  Maintainer : ijm
//     Lastmod : 20110822 (ijm)
//     License :
//  Copyright (C) 2011 Ashima Arts. All rights reserved.
//  Distributed under the MIT License. See LICENSE file.
//  https://github.com/ashima/webgl-noise
//
float snoise(vec2 v) {

    // Precompute values for skewed triangular grid
    const vec4 C = vec4(0.211324865405187,
                        // (3.0-sqrt(3.0))/6.0
                        0.366025403784439,
                        // 0.5*(sqrt(3.0)-1.0)
                        -0.577350269189626,
                        // -1.0 + 2.0 * C.x
                        0.024390243902439);
                        // 1.0 / 41.0

    // First corner (x0)
    vec2 i  = floor(v + dot(v, C.yy));
    vec2 x0 = v - i + dot(i, C.xx);

    // Other two corners (x1, x2)
    vec2 i1 = vec2(0.0);
    i1 = (x0.x > x0.y)? vec2(1.0, 0.0):vec2(0.0, 1.0);
    vec2 x1 = x0.xy + C.xx - i1;
    vec2 x2 = x0.xy + C.zz;

    // Do some permutations to avoid
    // truncation effects in permutation
    i = mod289(i);
    vec3 p = permute(
            permute( i.y + vec3(0.0, i1.y, 1.0))
                + i.x + vec3(0.0, i1.x, 1.0 ));

    vec3 m = max(0.5 - vec3(
                        dot(x0,x0),
                        dot(x1,x1),
                        dot(x2,x2)
                        ), 0.0);

    m = m*m ;
    m = m*m ;

    // Gradients:
    //  41 pts uniformly over a line, mapped onto a diamond
    //  The ring size 17*17 = 289 is close to a multiple
    //      of 41 (41*7 = 287)

    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;

    // Normalise gradients implicitly by scaling m
    // Approximation of: m *= inversesqrt(a0*a0 + h*h);
    m *= 1.79284291400159 - 0.85373472095314 * (a0*a0+h*h);

    // Compute final noise value at P
    vec3 g = vec3(0.0);
    g.x  = a0.x  * x0.x  + h.x  * x0.y;
    g.yz = a0.yz * vec2(x1.x,x2.x) + h.yz * vec2(x1.y,x2.y);
    return 130.0 * dot(m, g);
}

좀더 고른 분포의 랜던 hash

uvec2 murmurHash21(uint src) {
  const uint M = 0x5bd1e995u;
  uvec2 h = uvec2(1190494759u, 2147483647u);
  src *= M;
  src ^= src>>24u;
  src *= M;
  h *= M;
  h ^= src;
  h ^= h>>13u;
  h *= M;
  h ^= h>>15u;
  return h;
}

// 2 outputs, 1 input
vec2 hash21(float src) {
  uvec2 h = murmurHash21(floatBitsToUint(src));
  return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0;
}

smoothstep의 보간식

아래의 a와 b는 동일한 값이다.

float f = uv.x;
float a = smoothstep(0., 1., f);
float b = f * f * (3. - 2. * f);

위의 보간식을 cubic Hermite curve(큐빅 헤르미트 곡선)라고 한다. 0~1 사이의 영역에서 시작과 끝을 좀더 편평하게 만들어주는 quintic interpolation cuver(퀸틱 보간 곡선)에 대한 코드는 다음과 같다.

float f = uv.x;
float b = f * f * f * (6. * f * f - 15. * f + 10.);

modelMatrix로 변환된 normal 얻기

varying vec3 vNormal;

...

vNormal = mat3(transpose(inverse(modelMatrix))) * normal;

프레그먼트 쉐이더에서는 받은 vNormal을 반드시 정규화(normalize)해야 한다.

3초 주기로 0 ~ 1 사이의 연속된 값 얻기

uniform float u_time; // 0, 1, 2, ...의 값이며 각각 0초, 1초, 2초, ...를 나타냄

...

float period = 3.; // 3초 주기
float t = mod(u_time, period) / period; // 0 ~ 1 사이의 연속된 값

// sin 함수에 3.1415를 곱하는 이유는 sin 함수의 한 주기가 360도이기 때문임
vec3 color = mix(vec3(0), vec3(1), abs(sin(3.1415 * t)));
// or
vec3 color = mix(vec3(0), vec3(1), sin(3.1415 * t) * .5 + .5); 

원하는 각도를 이루는 선

아래의 이미지는 45도를 이루는 선인데, 이처럼 원하는 각도를 이루는 선을 만들기 위한 코드이다.

uniform vec3 uResolution;
uniform float uTime;
uniform vec4 uMouse;

#define PI (3.141592)

void main() {
  vec2 st = gl_FragCoord.xy / uResolution.xy;
  st = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;

  float w = fwidth(st.y);
  float a = (45.) * PI / 180.0;
  float d = dot(st, vec2(cos(a), sin(a)));
  d = smoothstep(-w, w, abs(d));

  gl_FragColor = vec4(vec3(1. - d), 1.);
}

다각형 그리기

uniform vec3 uResolution;
uniform float uTime;
uniform vec4 uMouse;

#define PI 3.14159265359
#define TWO_PI 6.28318530718

void main(){
  vec2 st = gl_FragCoord.xy/uResolution.xy * 2. - 1.; //-1-1
  st.x *= uResolution.x/uResolution.y;
  vec3 color = vec3(0.0);
  float d = 0.0;

  // Number of sides of your shape
  int N = 6;
  // int N = int(floor(mod(uTime * 10., 20.))) + 3;

  // Angle and radius from the current pixel
  float a = atan(st.x,st.y)+PI;
  float r = TWO_PI/float(N);

  // Shaping function that modulate the distance
  d = cos(floor(.5+a/r)*r-a)*length(st);

  // color = vec3(d); // Distance Field
  color = vec3(1.0-step(.5, d)); // Fill
  // color = vec3(step(.5,d) * step(d,.51)); // Only outline

  gl_FragColor = vec4(color,1.0);
}

멀티 카메라의 렌더링 결과를 하나의 Scene에 표시 (in Three.js)

여러 개의 카메라의 렌더링 결과를 하나의 장면에 표시해야할 필요가 생긴다. 예를들어 스타크래프트 게임을 보면 화면에 2개의 요소로 구분되어 있다. 첫째는 미니맵과 명령 아이콘들이 표시되는 커멘트 요소와 두번째는 실제 게임 플레이 요소이다. 이를 구현하는 방식은 여러가지가 있겠으나 멀티 카메라를 이용하는 방식이 있다. 게임 플레이 요소는 원근감 있게 PerspectiveCamera를 이용하고 커멘드 요소는 OrthographicCamera로 원근감 없이 렌더링하는 것이다. 이렇게 각 카메라로 렌더링되는 요소를 하나의 장면에 중첩해서 표시하면 된다.

마우스에 대한 상호작용은 Camera를 입력값을 개별 요소로 받으니 어떤 객체든 마우스 상호작용을 쉽게 얻을 수 있다. 이미 알고 있겠지만 Raycast를 이용해서 말이다. 여러개의 카메라를 사용할때 생각할 것은 어떤 요소를 어떤 카메라에 할당해 렌더링할 것인지에 대한 지정이다. 물론 하나의 요소를 여러개의 카메라에서 동시에 렌더링하는 것도 가능하다. 이러한 구분은 Layers라는 기능을 이용하면 매우 직관적으로 처리할 수 있다. three.js에서 Layers는 총 32개로 구성된다. 기본적으로 모든 카메라, 광원, 매시 등은 0번째 레이어에 소속된다. 어떤 카메라가 소속된 레이어와 동일한 레이어에 소속된 것들은 모두 카메라를 통해 렌더링된다.

이를 코드로 작성해 보자. 먼저 카메라 2개를 만들자. 쉽게 설명하기 위해 둘다 PerpectiveCamera 객체다.

_setupCamera() {
  ...
  
  const aspect = width / height;

  const camera1 = new THREE.PerspectiveCamera(60, aspect, 0.1, 10);
  camera1.layers.enable(1);
  camera1.position.z = 3;
  this._camera1 = camera1;

  const camera2 = new THREE.PerspectiveCamera(60, aspect, 0.1, 10);
  camera2.layers.enable(2);
  camera2.position.z = 2;
  this._camera2 = camera2;
}

camera1과 camera2는 각각 0,1 레이어와 0,2 레이어에 소속되어 있다. 만약 camera1을 1번 레이어에만 소속시키려면 camera1.layers.set(1) 코드면 된다.

다음은 렌더링할 매시를 다음처럼 구성한다.

const geometryC1 = new THREE.BoxGeometry();
const materialC1 = new THREE.MeshStandardMaterial();
const meshC1 = new THREE.Mesh(geometryC1, materialC1);
meshC1.layers.set(1);
this._scene.add(meshC1);

const geometryC2 = new THREE.BoxGeometry();
const materialC2 = new THREE.MeshStandardMaterial({ wireframe: true });
const meshC2 = new THREE.Mesh(geometryC2, materialC2)
meshC2.layers.set(2);
this._scene.add(meshC2);

meshC1은 1 레이어에만 소속되어 있다. meshC2는 2 레이어에만 소속되어 있다. 만약 layers의 set 대신 enable를 사용하면 기본적으로 소속된 0번 레이어에 대한 소속은 그대로 유지되므로 명확히 1 번 레이어에만 소속되도록 set를 사용했다. 광원에 대한 layers 설정은 하지 않았으므로 0 레이어에 소속되어 있다. camera1과 camera2의 layers를 enable로 해서 설정했으므로 각 카메라는 기본적으로 소속된 0번 레이어에도 소속되어 있고 0 레이어에 소속된 광원의 영향을 2개 카메라 모두 사용하게 된다.

이제 렌더링과 관련된 코드를 작성해야 한다. 그전에 Renderer 객체에 대해 다음 코드가 필요하다.

renderer.autoClearColor = false;

기본적으로 장면이 렌더링 되기 직전에 자동으로 정해진 색상으로 프레임버퍼가 설정된다. 위의 코드는 이처럼 자동으로 이뤄지는 것을 방지하고자 함이다. 자, 이제 렌더링 코드를 보자.

render() {
  this.update();

  this._renderer.clearColor();
  this._renderer.render(this._scene, this._camera2);
  this._renderer.render(this._scene, this._camera1);

  requestAnimationFrame(this.render.bind(this));
}

Renderer의 autoClearColor를 false로 지정했으므로 이제 직접 프레임버퍼를 지우기 위해 Renderer의 clearColor 매서드를 호출해야 한다. 그런 후에 각 카메라를 이용해 장면을 렌더링한다. 끝이다.

굳히 언급하지 않아도 이미 알겠지만 창 크기가 변경되면 카메라의 기저 인자값들을 재설정해줘야 한다. 카메라가 여러개이므로 이에 대한 코드까지 살펴보고 마무리 한다.

resize() {
  ...

  const aspect = width / height;

  this._camera1.aspect = aspect;
  this._camera1.updateProjectionMatrix();

  this._camera2.aspect = aspect;
  this._camera2.updateProjectionMatrix();

  ...
}