[THREE.JS] Blooming Earth

원하는 결과는 다음과 같습니다.

장면에 추가된 모델은 3개입니다. 지구 본체, 지구 주변의 푸른 빛(Blooming Light), 지구 주위의 하얀 작은 무수한 별들.

먼저 지구 본체에 대한 코드입니다.

const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(5, 50, 50), 
    new THREE.ShaderMaterial({
        uniforms: {
            globeTexture: {
                value: new THREE.TextureLoader().load("data/earth.jpg")
            }
        },
        vertexShader: `
            varying vec2 vertexUV;
            varying vec3 vertexNormal;

            void main() {
                vertexUV = uv;
                vertexNormal = normalize(normalMatrix * normal);

                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }
        `,
        fragmentShader: `
            uniform sampler2D globeTexture;

            varying vec2 vertexUV;

            void main() {
                vec4 color = texture2D(globeTexture, vertexUV);
                gl_FragColor = vec4(color.xyz, 1.);
            }
        `
    })
);

결과는 다음과 같습니다.

지구 가장자리가 어두워서 가장자리를 밝게 만들기 위해 위의 코드에서 fragmentShader 코드를 다음처럼 변경합니다.

fragmentShader: `
    uniform sampler2D globeTexture;

    varying vec2 vertexUV;
    varying vec3 vertexNormal;

    void main() {
        float intensity = 1.05 - dot(vertexNormal, vec3(0.,0.,1.));
        vec3 atmosphere = vec3(0.3, 0.6, 1.0) * pow(intensity, 1.5);

        vec4 color = texture2D(globeTexture, vertexUV);
        gl_FragColor = vec4(atmosphere + color.xyz, 1.);
    }
`,

결과는 다음과 같습니다.

지구 주변의 푸른 빛(Blooming Light)에 대한 코드입니다.

const atmosphere = new THREE.Mesh(
    new THREE.SphereGeometry(5, 50, 50),
    new THREE.ShaderMaterial({
        vertexShader: `
            varying vec3 vertexNormal;

            void main() {
                vertexNormal = normalize(normalMatrix * normal);
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }
        `,
        fragmentShader: `
            varying vec3 vertexNormal;

            void main() {
                float intensity = pow(0.76 - dot(vertexNormal, vec3(0,0,1.)), 2.0);
                gl_FragColor = vec4(0.3, 0.6, 1.0, 1) * intensity;
            }
        `,
        transparent: true,
        blending: THREE.AdditiveBlending,
        side: THREE.BackSide
    })
);
atmosphere.scale.set(1.2, 1.2, 1.2);

결과는 다음과 같습니다.

이제 지구 주위의 하얀 작은 무수한 별들에 대한 코드입니다.

const starGeometry = new THREE.BufferGeometry();

const starVertices = [];
for(let i=0; i<10000; i++) {
    const x = (Math.random() - 0.5) * 1000; 
    const y = (Math.random() - 0.5) * 1000; 
    const z = (Math.random() - 0.5) * 1000; 
    starVertices.push(x, y, z);
}

starGeometry.setAttribute("position", new THREE.Float32BufferAttribute(starVertices, 3));

const starMaterial = new THREE.PointsMaterial({color: 0xffffff});
const stars = new THREE.Points(starGeometry, starMaterial);
this._scene.add(stars);

결과는 이 글의 가장 첫번째 이미지와 같습니다.

CryptoJS를 이용한 AES256 암호화 / 복호화

자바스크립트에서 CryptoJS 라이브러리를 사용하여 AES256 방식으로 데이터를 암호화하기 위한 함수는 다음과 같습니다.

encodeByAES56(key, data){
    const cipher = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(key), {
        iv: CryptoJS.enc.Utf8.parse(""),
        padding: CryptoJS.pad.Pkcs7,
        mode: CryptoJS.mode.CBC
    });

    return cipher.toString();
}

위의 함수를 사용하는 예는 다음과 같구요.

const k = "key";
const rk = k.padEnd(32, " "); // AES256은 key 길이가 32자여야 함
const b = "암호화는 보안을 위해 매우 중요합니다.";
const eb = this.encodeByAES56(rk, b);

console.log(eb);

출력 결과는 다음과 같습니다.

mXK9nlcT70QdTjKgzxAD99zS4UYah0OZI8GFT8Pg+Vu3GFmF/HPmlV0PA/sUy7rr4+2Sh319ZEIz2TlyiPWcvw==

암호화된 위의 문자열을 복호화하는 함수는 다음과 같습니다.

decodeByAES256(key, data){
    const cipher = CryptoJS.AES.decrypt(data, CryptoJS.enc.Utf8.parse(key), {
        iv: CryptoJS.enc.Utf8.parse(""),
        padding: CryptoJS.pad.Pkcs7,
        mode: CryptoJS.mode.CBC
    });

    return cipher.toString(CryptoJS.enc.Utf8);
};

위의 함수를 사용하는 예는 다음과 같구요.

const k = "key"; // 암호화에서 사용한 값과 동일하게 해야함
const rk = k.padEnd(32, " "); // AES256은 key 길이가 32자여야 함
const eb = "mXK9nlcT70QdTjKgzxAD99zS4UYah0OZI8GFT8Pg+Vu3GFmF/HPmlV0PA/sUy7rr4+2Sh319ZEIz2TlyiPWcvw==";
const b = this.decodeByAES56(rk, eb);

console.log(b);

실행 결과는 다음과 같습니다.

암호화는 보안을 위해 매우 중요합니다.

주말에 THREE.JS를 이용해 만든 것

제 유튜브 채널을 통해 강좌로 소개할 것들에 대해 먼저 결과 만을 찍어 먼저 올려 봅니다. 아마 바로 돌아오는 이번주 토요일 쯤.. 아래 2개 중 한 개를 찍어 올릴 듯 합니다.

제 유튜브 채널 이름은 GIS Developer 입니다. 채널을 구독하시면 영상 업로드 시 바로 알 수 있습니다.

Lerp, InvLerp, Remap 함수 코드 및 three.js에서의 적용

언어는 C#인가 C인가.. Java인가.. 다 해당되는거 같기도 한데.. 여튼 워낙 기초 코드로 작성된 함수이니 저장해 두고 three.js의 쉐이더 작성 코드에서 적용한 예를 살펴봅니다.

float Lerp(float a, float b, float t) { // 내장 함수 mix와 동일
    return (1.0f - t) * a + b * t;
}

float InvLerp(float a, float b, float v) {
    return (v-a) / (b-a);
}

float Remap(float iMin, float iMax, float oMin, float oMax, float v) {
    float t = InvLerp(iMin, iMax, v);
    return Lerp(oMin, oMax, t);
}

만약 Remap 함수만 필요할 경우 다음 코드를 사용하면 됩니다.

float remap(float iMin, float iMax, float oMin, float oMax, float v) {
    float t = (v-iMin) / (iMax-iMin);
    return mix(oMin, oMax, t);
}

위의 코드는 Shader 중 glsl 언어로도 사용되는데요. 저 같은 경우 three.js에서 사용한 경우를 소개해 봅니다. 참고로 three.js은 WebGL 기술을 랩핑한 js 라이브러리입니다.

Shader는 Vertex와 Fragment에 대한 처리가 있고.. 재질(Material)에 지정되는데요. 다시 재질은 적용될 지오메트리(Geometry)가 필요합니다. 다음은 지오메트리와 재질에 대한 코드입니다.

const geometry = new THREE.BoxGeometry(1, 1, 1, 10, 10, 10);

fetch("shader4.glsl").then(response => {
    return response.text();
}).then(text => {
    const fragmentShaerCode = text;
    const material = new THREE.ShaderMaterial({
        side: THREE.DoubleSide,
        transparent: true,

        uniforms: {
            iTime: { value: 0 },
            iResolution:  { value: new THREE.Vector3() },
        },

        vertexShader: `
            uniform float iTime;    
        
            varying vec2 vUv;

            void main() {
                vUv = uv;
                gl_Position = projectionMatrix * modelViewMatrix  * vec4(position,1.0);
                //gl_Position = projectionMatrix*(modelViewMatrix*vec4(0.,0.,0.,1.)+vec4(position.x,position.y,0.,0.)); // <- billboard
            }
        `,
        
        fragmentShader: fragmentShaerCode
    });

    const cube = new THREE.Mesh(geometry, material);
    this._scene.add(cube);

    this._material = material;

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

fragmentShaerCode는 shader4.glsl 파일로부터 불러와지는데, 해당 코드는 다음과 같습니다.

uniform vec3 iResolution;
uniform float iTime;

varying vec2 vUv;

float rect(vec2 uv, vec2 p0, vec2 p1, float blur) {
    float maskX = smoothstep(p0.x-blur, p0.x+blur, uv.x);
    maskX -= smoothstep(p1.x-blur, p1.x+blur, uv.x);

    float maskY = smoothstep(p0.y-blur, p0.y+blur, uv.y);
    maskY -= smoothstep(p1.y-blur, p1.y+blur, uv.y);

    float mask = maskX * maskY;
    return mask;
}

// t = a -> return 0., t = b -> return 1.
float invLerp(float a, float b, float t) {
    return (a - t) / (a - b);
}

float remap(float a, float b, float c, float d, float t) {
    return invLerp(a, b, t) * (d - c) + c;
}

void main() {
    vec2 cUv = vUv;

    cUv -= .5;
    float x = cUv.x;

    float m = sin(iTime+x*8.) * .1;
    float y = cUv.y - m;

    float blur = remap(-.5, .5, .01, .25, x);
    blur = pow(blur*3., 3.);
    float mask = rect(vec2(x, y), vec2(-.5,-.1), vec2(.5,.1), blur);

    vec3 col = vec3(1., 1., 0.) * mask;
    gl_FragColor = vec4(col, 1. ); 
}

추후 이 내용을 제 스스로도 참조하기 위해 재질에 대한 uniforms 데이터를 지정하고 있는데요. 관련 코드는 다음과 같습니다.

update(time) {
    time *= 0.001; // second unit
    this._material.uniforms.iTime.value = time;
}

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

    ...

    this._material.uniforms.iResolution.value.set(width, height, 1);
}

위의 코드에 대한 실행 결과는 다음과 같습니다.

위의 예제는 YouTube의 The Art Of Code의 영상을 참조하여 three.js에 맞게 적용한 것입니다.

쉐이더(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)을 불러와 사용하고 있습니다.