curlNoise 와 Line Effect 연구

컬노이즈와 라인 이펙트를 활용하면 굉장히 멋진 비주얼 효과를 만들어 낼 수 있습니다.

다만 블렌더나, 후니디 같은 그래픽스 프로그램의 도움 말고 직접 원리를 알아내여 구현해 보려고 연구하였습니다.

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import vertexShader from '../src/shaders/curl/vertex.glsl'
import fragmentShader from '../src/shaders/curl/fragment.glsl'
import { createNoise3D } from 'simplex-noise'
import GUI from 'lil-gui'


let time = 0;
let texLoader = new THREE.TextureLoader;
let tex = texLoader.load('./test.jpg');
let rayCaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let eMouse = new THREE.Vector2();
let elasticMouse = new THREE.Vector2(0, 0);
let elasticMouseVel = new THREE.Vector2(0, 0);
let light;
let material;
const colors = [new THREE.Color(0xfa7aff), new THREE.Color(0x7a83ff), new THREE.Color(0xfff7c2)];
let materials = [];

// Debug
const gui = new GUI({ width: 340 });
const debugObject = {};

// Canvas
const canvas = window.document.querySelector('canvas.webgl');

// Scene
const scene = new THREE.Scene();

// Loader
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');

const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);

// Sizes
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight,
    pixelRatio: Math.min(window.devicePixelRatio, 2)
};

window.addEventListener('resize', () => {
    // Update sizes
    sizes.width = window.innerWidth;
    sizes.height = window.innerHeight;
    sizes.pixelRatio = Math.min(window.devicePixelRatio, 2);

    // Update camera
    camera.aspect = sizes.width / sizes.height;
    camera.updateProjectionMatrix();

    // Update renderer
    renderer.setSize(sizes.width, sizes.height);
    renderer.setPixelRatio(sizes.pixelRatio);
});

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100);
camera.position.set(0, 0, 10);
scene.add(camera);

// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    antialias: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(sizes.pixelRatio);

debugObject.clearColor = '#ffffff';
renderer.setClearColor(debugObject.clearColor);

const clock = new THREE.Clock();
let previousTime = 0;

const tick = () => {
    const elapsedTime = clock.getElapsedTime();
    const deltaTime = elapsedTime - previousTime;
    previousTime = elapsedTime;    
    controls.update();

    // 메터리얼 유니폼 업데이트
    materials.forEach(material => {
        material.uniforms.uTime.value += deltaTime * 5.0;
        // material.uniforms.uLight.value = light.position;
    });

    renderer.render(scene, camera);
    window.requestAnimationFrame(tick);
};

const getCurve = (start) => {
    let scale = 1;
    let points = [];
    points.push(start);

    let currentPoint = start.clone();

    for (let i = 0; i < 400; i++) {
        let v = computeCurl(currentPoint.x / scale, currentPoint.y / scale, currentPoint.z / scale);
        
        currentPoint.addScaledVector(v, 0.002);
        points.push(currentPoint.clone());
    }
    return points;
};

const addObject = () => {
    materials = colors.map(color => 
        new THREE.ShaderMaterial({
            uniforms: {
                uTime: { value: 0 },
                resolution: { value: new THREE.Vector4() },
                uLight: { value: new THREE.Vector3() },
                uColor: { value: color } // 각 메터리얼에 색상 추가
            },
            side: THREE.DoubleSide,
            vertexShader: vertexShader,
            fragmentShader: fragmentShader,
            // wireframe: true
        })
    );

    for (let i = 0; i < 1000; i++) {
        let path = new THREE.CatmullRomCurve3(
            getCurve(new THREE.Vector3(
                Math.random() - 0.5,
                Math.random() - 0.5,
                Math.random() - 0.5
            ))
        );
        let geometry = new THREE.TubeGeometry(path, 400, 0.005, 8, false);

        // 랜덤한 메터리얼 선택
        const randomMaterial = materials[Math.floor(Math.random() * materials.length)];

        const curve = new THREE.Mesh(geometry, randomMaterial);
        scene.add(curve);
    }
};

// 노이즈 함수
const simplex3D = createNoise3D();
function computeCurl(x, y, z) {
    var eps = 0.0001;
    var curl = new THREE.Vector3();

    //Find rate of change in YZ plane
    var n1 = simplex3D(x, y + eps, z); 
    var n2 = simplex3D(x, y - eps, z); 
    var a = (n1 - n2) / (2 * eps);
    var n1 = simplex3D(x, y, z + eps); 
    var n2 = simplex3D(x, y, z - eps); 
    var b = (n1 - n2) / (2 * eps);
    curl.x = a - b;

    //Find rate of change in XZ plane
    n1 = simplex3D(x, y, z + eps); 
    n2 = simplex3D(x, y, z - eps); 
    a = (n1 - n2) / (2 * eps);
    n1 = simplex3D(x + eps, y, z); 
    n2 = simplex3D(x - eps, y, z); 
    b = (n1 - n2) / (2 * eps);
    curl.y = a - b;

    //Find rate of change in XY plane
    n1 = simplex3D(x + eps, y, z); 
    n2 = simplex3D(x - eps, y, z); 
    a = (n1 - n2) / (2 * eps);
    n1 = simplex3D(x, y + eps, z); 
    n2 = simplex3D(x, y - eps, z); 
    b = (n1 - n2) / (2 * eps);
    curl.z = a - b;

    return curl;
}

const rayCast = () => {
    let raycastPlane = new THREE.Mesh(
        new THREE.PlaneGeometry(10, 10),
        new THREE.MeshBasicMaterial({ color: 0xff0000 })
    );

    light = new THREE.Mesh(
        new THREE.SphereGeometry(0.1, 20, 20),
        new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    );
    scene.add(raycastPlane);
    scene.add(light);

    window.addEventListener('mousemove', (event) => {
        mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
        rayCaster.setFromCamera(mouse, camera);
        eMouse.x = event.clientX;
        eMouse.y = event.clientY;

        const intersects = rayCaster.intersectObjects([raycastPlane]);
        if (intersects.length > 0) {
            let p = intersects[0].point;
            console.log(p);
            light.position.copy(p);
        }
    });
};

// rayCast();
addObject();
tick();
// 프래그먼트 쉐이더
varying vec2 vUv;
varying vec3 vPosition;
uniform vec3 uColor;
uniform vec3 uLight;
uniform float uTime;

void main() {
    float dash = sin(vUv.x * 20.0 + uTime);
    if (dash < 0.01) discard;
    gl_FragColor = vec4(uColor, 1.0);
}
Next
Next

WebGL에서 그림자를 포함한 ToonShader 연구