쉐이더(Shader) 코드를 외부 자원으로 분리하기

Shader 코드를 외부 자원으로 분리해서 좀 더 깔끔하게 코드를 작성하고자 합니다. 물론 이런 분리는 유지보수 및 Shader에 대한 분리를 통해 유연성을 증가시키는 장점도 있습니다. Shader 코드를 shader.glsl이라는 파일로 작성했다고 할 때, 이 파일 자원을 불러와 사용하는 코드의 예시는 다음과 같습니다.

fetch("shader.glsl").then(response => {
    return response.text();
}).then(text => {
    const fragmentShaerCode = text;
    const material = new THREE.ShaderMaterial({

        ...

        vertexShader: `
            uniform float iTime;    
        
            varying vec2 vUv;

            void main() {
                vUv = uv;
                gl_Position = projectionMatrix * modelViewMatrix  * vec4(position,1.0);
            }
        `,
        
        fragmentShader: fragmentShaerCode
    });

    ....

}).catch(function (error) {
    console.warn(error);
});

vertex Shader는 분리되어 있지 않았지만 fragment Shader는 분리된 파일 자원(여기서는 shader.glsl)을 불러와 사용하고 있습니다.

원하는 매쉬에 대한 에니메이션 ZoomIn 함수

먼저 사용하는 함수는 다음과 같습니다. (에니메이션을 위하 GSAP 라이브러리를 사용하였음)

_zoomFit(object3D, viewMode, bFront, viewAngle) {
    const box = new THREE.Box3().setFromObject(object3D);
    const sizeBox = box.getSize(new THREE.Vector3()).length();
    const centerBox = box.getCenter(new THREE.Vector3());
    
    const offset = new THREE.Vector3(viewMode==="X"?1:0, viewMode==="Y"?1:0, viewMode==="Z"?1:0);
    if(!bFront) offset.negate();
    offset.applyAxisAngle(new THREE.Vector3(1,0,0), THREE.Math.degToRad(viewAngle));

    const newPosition = new THREE.Vector3().copy(centerBox).add(offset);
    const halfSizeModel = sizeBox * 0.5;
    const halfFov = THREE.Math.degToRad(this._camera.fov * .5);
    const distance = halfSizeModel / Math.tan(halfFov);
    const direction = (new THREE.Vector3()).subVectors(newPosition, centerBox).normalize();
    newPosition.copy(direction.multiplyScalar(distance).add(centerBox));

    const oldPosition = this._camera.position.clone();
    gsap.to(this._camera.position, { duration: 0.5, x: newPosition.x, y: newPosition.y, z: newPosition.z});

    // camera.lookAt(centerBox.x, centerBox.y, centerBox.z); // OrbitControls를 사용하지 않은 경우
    // this._controls.target.copy(centerBox); // OrbitControls를 사용하고 있는 경우
    gsap.to(this._controls.target, { duration: 0.5,  
        x: centerBox.x, y: centerBox.y, z: centerBox.z});        
}

사용은 줌인 대상을 마우스로 클릭해 선택한다고 할때.. 먼저 RayCaster 객체를 하나 정의하구요.

_setupPicking() {
    const raycaster = new THREE.Raycaster();
    this._divContainer.addEventListener("click", this._onClick.bind(this));
    this._raycaster = raycaster;
}

클릭 이벤트인 _onClick은 다음과 같습니다.

_onClick(event) {
    if(!event.ctrlKey) return false;
    const width = this._divContainer.clientWidth;
    const height = this._divContainer.clientHeight;
    const xy = {x: (event.offsetX / width) * 2 - 1, y: -(event.offsetY / height) * 2 + 1};
    this._raycaster.setFromCamera(xy, this._camera);
    const targets = this._raycaster.intersectObjects(this._scene.children);
    if(targets.length > 0) {
        const mesh = targets[0].object;
        this._zoomFit(mesh, "Y", true, 50);
    }
}

이 애니메이션 ZoomIn 기능을 이용해 만든 단위 기능에 대한 예제 영상은 다음과 같습니다.