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