voro-noise

노이즈는 렌덤을 기반으로 하며 FBM(Fractal Brownian Motion)을 위한 한 옥타브를 구성합니다. 몇가지 노이즈 중 하나로 보로노이(Voronoi)를 기반으로 하는 voro-noise가 있는데, Shader의 대가인 Inigo님이 2014년에 만든 알고리즘입니다. 내부 코드를 보면 퍼퍼먼스에 다소 부정적인 부분(일반적인 9개의 격자 그리드가 아닌 25개의 격자 그리드를 사용)이 보이지만 그 결과는 여타 다른 노이즈보다 훨씬 뛰어납니다.

아래의 코드는 voro-noise에 대한 구현 코드입니다.

vec3 hash3(vec2 p) {
    vec3 q = vec3(
        dot(p, vec2(127.1, 311.7)), 
        dot(p, vec2(269.5, 183.3)), 
        dot(p, vec2(419.2, 371.9))
    );
    return fract(sin(q) * 43758.5453);
}

float voronoise(in vec2 p, float u, float v) {
    float k = 1.0 + 63.0 * pow(1.0 - v, 6.0);

    vec2 i = floor(p);
    vec2 f = fract(p);

    vec2 a = vec2(0.0, 0.0);
    for(int y = -2; y <= 2; y++) {
        for(int x = -2; x <= 2; x++) {
            vec2 g = vec2(x, y);
            vec3 o = hash3(i + g) * vec3(u, u, 1.0);
            vec2 d = g - f + o.xy;
            float w = pow(1.0 - smoothstep(0.0, 1.414, length(d)), k);
            a += vec2(o.z * w, w);
        }
    }

    return a.x / a.y;
}

이 함수에 대한 가장 흔한 코드 예시는 다음과 같습니다.

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

    float f = voronoise(st, 1., 1.);
    gl_FragColor = vec4(f, f, f, 1.0);
}

위의 코드의 결과는 다음과 같구요.

voronoise 함수는 3개의 인자를 받는데, 첫번째 인자는 일반적으로 노이즈 함수가 받는 인자입니다. 2,3번째 인자인 u와 v는 voronoise에 특화된 인자인데 u는 노이즈 생성을 격자 그리드(Grid)를 얼마나 보로노이스럽게 표현할지에 대한 강도값으로 0에서 1까지의 값을 갖습니다. v는 격자 그리드 내부를 채우는 각 프레그먼트에 대한 보간 정도에 대한 강도 값인데 0부터 1의 값이며 0일때 전혀 보간이 이루어지지 않고 1일때 최대의 보간이 이뤄집니다. 아래의 결과는 u와 v를 모두 0으로 했을 때의 결과입니다.

아래는 u를 1로 v는 0으로 했을때의 결과입니다.

마지막으로 아래는 u를 0으로 v를 1로 했을때의 결과입니다.

아래는 노이즈 구현에 대한 유용한 사이트입니다.

https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83

경로 Mesh를 따라 흐르는 화살표 시각화 (three.js)

공장 생산 설비 시각화나 도로 진행 방향 시각화 등에서 자주 사용되는 기능 중 하나입니다. 아래처럼 간단한 경로를 Mesh로 구성하고 화살표 이미지를 맵핑한 후 원하는 방향으로 화살표가 흘러가도록 합니다.

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