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

GLSL 코드

다음과 같은 결과를 프레그먼트 쉐이더로 작성한다면 … ? 즉, 화면의 반을 가르고 가른 영역의 중심을 원점으로 삼으며 원점을 기준으로 gl_FragCoord 지점에 대한 좌표(x,y)에 대한 각도(atan(y,x))에 대해 왼쪽 영역은 cos 값으로, 오른쪽 영역은 sin 값으로 채움.

방식은 여러가지겠지만 여기서는 2가지 구현 코드를 언급함. 첫번째는 제시된 문제를 그대로 해석해 풀이한 것.

uniform vec2 iResolution;

void main2() {
    vec2 uv = gl_FragCoord.xy / iResolution;
  
    vec2 origin = uv.x < 0.5 ? vec2(0.25, 0.5) : vec2(0.75, 0.5);
    vec2 v = uv - origin;
  
    float angle = atan(v.y, v.x);
    float c = cos(angle);
    float s = sin(angle);
    
    vec3 color = uv.x < 0.5 ? vec3(c) : vec3(s);
    
    gl_FragColor = vec4(color, 1.0);
}

두번째는 삼각함수의 원리를 이해하고 상황에 맞게 최적화해 구현한 것.

uniform vec2 iResolution;

void main() {
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  vec2 normal = uv.x > .5 ? 
    normalize(uv - vec2(0.75, 0.5)) : 
    normalize(uv - vec2(0.25, 0.5));
  
  float t = uv.x > .5 ?  normal.y : normal.x;

  gl_FragColor = vec4(vec3(t), 1.0);
}

three.js, 뒤에서 보여지는 물체 감추기

실내와 같은 장면을 3D로 살펴볼때 벽처럼 막힌 부분이 장면의 시인성을 방해하는 경우가 있습니다. 아래가 그러한 경우입니다.

위의 영상에서 보이는 것처럼 실내의 물체를 벽이 가리는 문제가 있습니다. 이런 문제점을 해결하기 위해 뒤에서 보여지는 물체(벽 등)를 잠시 보이지 않도록 해주는 기능이 필요한데요. 아래는 그에 대한 결과입니다.

이에 대한 기능을 컴포넌트로 만든 것이 BackViewHiderControls 입니다. 관련 API는 다음과 같습니다.

먼저 컨트롤러 객체를 생성합니다.

_setupControls() {
  ...

  const backViewHider = new BackViewHiderControls(this._camera);
  this._backViewHider = backViewHider;

  ...
}

그리고 매 프레임마다 호출되는 update 매서드에서 다음 코드를 입력합니다.

update() {
  ....

  this._backViewHider.updateOnFrame();
}

three.js, 선택된 모델에 대한 줌(Zoom)

3D 그래픽 기능 개발에 있어서 선택된 모델을 화면에서 확대하는 기능입니다.

ZoomControls 라는 클래스로 컴포넌트화 해서 재사용성을 높였습니다. 이 기능은 제 유튜브 채널의 강좌에서 설명하고 있는 코드를 거의 그대로 사용하고 있습니다.

ZoomControls에 대한 API 사용은 다음과 같습니다.

먼저 ZoomControls 객체를 생성합니다.

_setupControls() {
  this._orbitControls = new OrbitControls(this._camera, this._divContainer);
 
  ...

  const zoomControls = new ZoomControls(this._orbitControls);
  this._zoomControls = zoomControls;
}

그리고 확대하고자 하는 Object3D에 대해 다음처럼 코드를 수행해주면 됩니다.

zoom(zoomTarget, view) {
  if (view === "좌측 뷰") this._zoomControls.zoomLeft(zoomTarget);
  else if (view === "우측 뷰") this._zoomControls.zoomRight(zoomTarget);
  else if (view === "정면 뷰") this._zoomControls.zoomFront(zoomTarget);
  else if (view === "후면 뷰") this._zoomControls.zoomBack(zoomTarget);
  else if (view === "상단 뷰") this._zoomControls.zoomTop(zoomTarget);
  else if (view === "하단 뷰") this._zoomControls.zoomBottom(zoomTarget);
  else if (view === "뷰 유지") this._zoomControls.zoom(zoomTarget);
}

선택된 모델에 대한 아웃라인 표시는 SelectionPassWrapper API를 사용하였고 이와 관련된 내용은 다음과 같습니다.

three.js, 선택된 3D 모델에 대한 하이라이팅

three.js, 3D 모델에 대한 라벨 시각화

3D 모델에 대한 효과적은 라벨 시각화는 그 목표에 따라 매우 주관적이고 다양하게 접근할 수 있습니다. 이 글에서 제공되는 라벨 시각화 역시 많은 다양한 방법 중에 하나인데요. 목표는 3D 모델을 최대한 가리지 않아야 하며 라벨 사이의 충돌을 최소화 해야 합니다. 이런 목표에 따라 만든 기능은 아래와 같습니다.

위의 기능을 프로젝트에 빠르고 쉽게 적용하기 위해 SmartLabel이라는 이름으로 컴포넌트로 만들었는데요. 이 컴포넌트를 적용할 때 고려해야할 코드를 정리하면 다음과 같습니다.

먼저 SmartLabel 컴포넌트와 라벨 관련 데이터를 생성하고 설정해야 합니다.

  _setupLabels() {
    const smartLabel = new SmartLabel(this._scene, this._camera);
    this._smartLabel = smartLabel;

    const labels = {
      "Object": {
        label: "미할당영역",
        textColor: "gray",
      },
      "Part1": {
        label: "정문 계단",
        textColor: "white",
      },
      "Part2": {
        label: "정문",
        textColor: "white",
      },
      "Part3": {
        label: "파손영역A",
        textColor: "red",
      },
      "Part4": {
        label: "파손영역B",
        textColor: "red",
      },
      
      ...
    }

    this._smartLabel.setLabelData(labels);
  }

labels 객체의 key는 Mesh의 이름이고 Value는 표시될 라벨과 텍스트 색상입니다. 라벨이 표시될 Mesh의 지정은 다음과 같습니다.

_setupModel() {
  const loader = new GLTFLoader();
  loader.load("./model.glb", gltf => {
    const object = gltf.scene;
    this._scene.add(object);
    const meshes = [];

    object.traverse(obj => {
      if (obj.isMesh) meshes.push(obj);
    });

    this._smartLabel.setTargetMeshs(meshes);
    this._smartLabel.buildLabels();

    ...
  });
}

매 프레임마다 다음과 같은 코드 호출이 필요합니다.

update() {
  ...

  this._smartLabel.updateOnFrame();
}

렌더링 시 다음과 같은 코드 호출이 필요하구요.

render() {
  this.update();

  ...

  this._smartLabel.render();

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

렌더링 되는 영역의 크기가 변경되면 다음 코드 호출이 필요합니다.

resize() {
  const width = this._divContainer.clientWidth;
  const height = this._divContainer.clientHeight;

  ...  

  this._smartLabel.updateSize(width, height);
}

라벨은 DOM 요소입니다. 이에 대한 스타일이 필요할 수 있는데, 위의 예시의 경우 다음과 같습니다.

.label {
  font-size: 1em;
  border: 2px solid yellow;
  padding: 0.3em 0.8em;
  border-radius: 0.9em;

  background-color: black;
  transition: transform 0.2s ease-out, opacity 1s ease;
  box-shadow: 0 0 5px rgba(0,0,0,0.5);
}