
const sizes = { width: window.innerWidth, height: window.innerHeight, }; ... let camera = null; setCamera(); ... function setCamera() { const distance = 500; const fov = (180 * (2 * Math.atan(sizes.height / 2 / distance))) / Math.PI; camera = new THREE.PerspectiveCamera( fov, sizes.width / sizes.height, 1, 1000 ); camera.position.z = distance; scene.add(camera); }
μμμ distance μ€μ ν λ³μλ‘ λ§λ€κΈ° (μ μμ μμλ 500μΌλ‘ μ€μ )
cameraμ fov κ³μ°νκΈ° (λ§€μ° μ€μ)
const fov = (180 * (2 * Math.atan(height / 2 / distance))) / Math.PI;
Three.js μΊλ²μ€ μμμμ λμ΄λ₯Ό μΌμΉμν€κΈ° μν΄ height μ window.innerHeight μ¬μ© (μ μμ μμλ sizes.heightλ‘ λ체)distance κ° μ¬μ©νμ¬ fov κ³μ°νκΈ°κ³μ°λ fovλ₯Ό νμ©νμ¬ PerspectiveCamera μμ±νκΈ°
PerspectiveCameraμ near, farλ 1μμ 1000μΌλ‘ μ€μ (distanceλ₯Ό 500μΌλ‘ μ€μ νκΈ° λλ¬Έμ farλ₯Ό 500μ΄νλ‘ μ€μ νμκ²½μ° νλ©΄μμ 보μ΄μ§ μκ² λ©λλ€)
μΉ΄λ©λΌμ zμμΉλ₯Ό distance κ°μΌλ‘ μ€μ ν sceneμ μΆκ°νκΈ°
const links = document.querySelectorAll(".link-img"); ... let bounds = {}; ... init(); ... function init() { links.forEach((link, i) => { mesh = setMesh(i); setBounds(link); }); } function setMesh(i) { const geometry = new THREE.PlaneGeometry(1, 1, 100, 100); const material = new THREE.ShaderMaterial({ ... }); const plane = new THREE.Mesh(geometry, material); scene.add(plane); return plane; } function setBounds(link) { const rect = link.getBoundingClientRect(); const width = rect.width; const height = rect.height; const left = rect.left; const right = rect.right; const x = rect.left + rect.width / 2 - sizes.width / 2; const y = -(rect.top + rect.height / 2) + sizes.height / 2; bounds = { width, height, left, right, x, y }; mesh.scale.set(width, height, 1); mesh.position.set(x, y, 0); }
document.querySelectorAllμ μ¬μ©νμ¬ μμΉλ₯Ό ꡬν μμλ€μ μ ννκ³ λ³μλ‘ λ§λ€κΈ°
forEach λ°λ³΅λ¬Έ μ¬μ©νκΈ° (λ§€κ°λ³μλ‘ κ° μμ, μΈλ±μ€ μ¬μ©)
DOMμ μμΉλ₯Ό ꡬν setBounds ν¨μ λ§λ€κΈ° (κ° μμλ€, μ¦ linkλ₯Ό λ§€κ°λ³μλ‘ λ°μ)
getBoundingClientRect()λ₯Ό μ¬μ©νμ¬ κ° μμμ ν¬κΈ° λ° μμΉ μ 보 λ³μ rectμ μ μ₯νκΈ°
width, height, left, right, x, y κ°μ κ°κ° λ³μλ‘ λ§λ€κΈ°
x, y κ°μ κ° μμμ μ€μ¬ μμΉ κ° (Meshμ μμΉ μ€μ ν λμ μ¬μ©ν κ°)x : μ’μΈ‘ μμΉ + λλΉ / 2 - μλμ° λλΉ / 2 y : μλμ° λμ΄ / 2 - (μμͺ½ μμΉ + λμ΄ / 2)
κ°μ²΄ boundsμ μμμ ꡬν΄μ§ κ°λ€ μ μ₯νκΈ°
미리 λ§λ€μ΄ λμ meshμ scale κ°μ μμμ λλΉ, λμ΄ κ°μΌλ‘ μ€μ νκΈ° (z κ°μ 1λ‘ μ€μ )
boundsμ x, y κ°μ νμ©νμ¬ meshμ position μ€μ νκΈ° (z κ°μ 0μΌλ‘ μ€μ )
const state = { scroll: { target: 0, current: 0, }, ... }; ... window.addEventListener("wheel", (e) => { handleWheel(e); }); ... function handleWheel(e) { let delta = e.deltaY; delta *= 0.55; handleScroll(delta); } function handleScroll(delta) { state.scroll.target = Math.round(state.scroll.target + delta * 0.6); state.scroll.target = THREE.MathUtils.clamp( state.scroll.target, 0, sizes.width ); }
μ€ν¬λ‘€ λ° κΈ°ν μνλ₯Ό κ΄λ¦¬ν κ°μ²΄ state λ§λ€κΈ°
ν μ΄λ²€νΈμ μ¬μ©ν ν¨μ handleWheel λ§λ€κΈ° (λ§€κ°λ³μλ‘ μ΄λ²€νΈ e 보λ΄κΈ°)
λ³μ deltaμ e.deltaY κ° μ μνκΈ°
λ§μ°μ€ ν μ μμ§ μ΄λλμ μλ―Ένλ©°, μλλ‘ μ€ν¬λ‘€ νμλλ μμ κ°, μλ‘ μ€ν¬λ‘€ νμλλ μμ κ°μ λνλ λλ€.
deltaμ 0.55λ₯Ό κ³±νμ¬ μ΄λλμ μ‘°μ νκΈ°
deltaλ₯Ό λ§€κ°λ³μλ‘ νλ handleScroll ν¨μ λ§λ€κ³ , νΈμΆνκΈ°
deltaμ 0.6μ κ³±νμ¬ μ΄λλμ νλ² λ μ‘°μ ν ν stateμ μ€ν¬λ‘€ νκ²μ λν κ°μ Math.round()λ₯Ό μ¬μ©νμ¬ λ°μ¬λ¦Ό ν ν, κ·Έ κ°μ ν λΉνκΈ°
Three.jsμ λ΄μ₯ ν¨μ MathUtils.clampλ₯Ό μ¬μ©νμ¬ stateμ μ€ν¬λ‘€ νκ² κ°μ 0λΆν° sizes.width μ¦, μλμ° λλΉλ₯Ό λμ§ μλλ‘ κ³ μ μν€κΈ° (μλμ° λλΉλ μ μμ λ₯Ό μν΄ μμλ‘ μ€μ ν κ°)
THREE.MathUtils.clamp(value, min, max); // μ«μ κ°μ νΉμ λ²μ λ΄λ‘ μ ννλ λ° μ¬μ©λ©λλ€. μ¦, κ°μ΄ μ§μ λ μ΅μκ°κ³Ό μ΅λκ° μ¬μ΄μ μλλ‘ λ³΄μ₯ν©λλ€.
const uniforms = { uResolution: { value: new THREE.Vector2(sizes.width, sizes.height) }, uProgress: { value: 0 }, uEffectWidth: { value: 0.75 }, uStrength: { value: 0 }, }; const state = { ... , progress: { target: 0, current: -0.5, }, strength: { target: 0, current: 0, base: -0.8, }, }; const clock = new THREE.Clock(); let deltaTime = null; ... function update() { deltaTime = clock.getDelta() * 1000; const progress = THREE.MathUtils.mapLinear( state.scroll.target, 0, sizes.width, 0, 1 ); state.progress.target = progress - 0.5; state.progress.current = lerp( state.progress.current, state.progress.target, 0.09 ); if (Math.abs(state.progress.current - state.progress.target) < 0.02) { state.strength.target = 0; } else { state.strength.target = state.strength.base; } state.strength.current = lerp( state.strength.current, state.strength.target, 0.04 ); uniforms.uProgress.value = state.progress.current; uniforms.uStrength.value = state.strength.current; ... } function lerp(value, target, time) { return value + (target - value) * (time / (16.6666666666667 / deltaTime)); }
Shaderμμ μ¬μ©ν κ°λ€μ λ΄μ κ°μ²΄ uniforms λ§λ€κΈ°
const uniforms = { uResolution: { value: new THREE.Vector2(sizes.width, sizes.height) }, // Three.jsμ Vector2λ₯Ό νμ©νμ¬, νμ¬ νλ©΄μ ν΄μλ λ΄κΈ° uProgress: { value: 0 }, // μ€ν¬λ‘€μ μ§νλλ₯Ό λνλ΄λ κ° uEffectWidth: { value: 0.75 }, // λ¬Όκ²° ν¨κ³Όμ λλΉλ₯Ό μ νλ κ° uStrength: { value: 0 }, // μ€ν¬λ‘€ νμμ λ, λ¬Όκ²° ν¨κ³Όμ κ°λλ₯Ό μ νλ κ° };
μν κ΄λ¦¬ κ°μ²΄ stateμ progress, strength νλͺ© μΆκ°νκΈ°
Three.jsμ Clockμ μ¬μ©νμ¬ νλ μ κ° μκ° κ²½κ³Όλ₯Ό μΈ‘μ νκΈ°
νμ¬μ νλ μ λ μ΄νΈλ₯Ό λ΄μ λ³μ deltaTime λ§λ€κΈ°
λ λλ§μ μ¬μ©ν κ°λ€μ μ€μκ°μΌλ‘ μ
λ°μ΄νΈ ν ν¨μ update() λ§λ€κΈ°
Clockμ getDelta()λ₯Ό μ¬μ©νμ¬ νμ¬μ νλ μ λ μ΄νΈλ₯Ό μ€μκ°μΌλ‘ μ
λ°μ΄νΈ νκΈ°
νμ¬μ μ€ν¬λ‘€ μ§νλλ₯Ό 0λΆν° 1κΉμ§μ κ°μΌλ‘ λ°κΎΈκΈ° μν΄ Three.jsμ λ΄μ₯ ν¨μ MathUtils.mapLinearλ₯Ό μ¬μ©νκΈ°
THREE.MathUtils.mapLinear(x, a1, a2, b1, b2); // x: λ³νν μ λ ₯ κ° // a1: μ λ ₯ κ°μ μ΅μκ° // a2: μ λ ₯ κ°μ μ΅λκ° // b1: μΆλ ₯ κ°μ μ΅μκ° // b2: μΆλ ₯ κ°μ μ΅λκ° // μ£Όμ΄μ§ μ λ ₯ λ²μμμ ν΄λΉ κ°μ΄ μ°¨μ§νλ λΉμ¨μ κΈ°μ€μΌλ‘ μλ‘μ΄ μΆλ ₯ λ²μμ κ°μ κ³μ°ν©λλ€.
λ§΅νλ κ°μ λ΄μ λ³μ progressμ -0.5 λ§νΌ λΊ κ°μ stateμ progress νκ² κ°μΌλ‘ μ€μ (0μ μ€κ° κ°μΌλ‘ λ§λ€κΈ° μν΄μ)
ν¨μ lerpλ₯Ό μ¬μ©νμ¬ stateμ progress νμ¬ κ°μ νκ² κ°μΌλ‘ λΆλλ½κ² 보κ°νκΈ°
function lerp(value, target, time) { return value + (target - value) * (time / (16.6666666666667 / deltaTime)); } // 60νλ μ(μ½ 16.67ms)μ deltaTimeμΌλ‘ λλμΌλ‘μ¨, κ°μ λ³ν μλλ₯Ό μΌμ νκ² μ μ§ // lerp ν¨μλ μ ν 보κ°(Linear Interpolation)μ ꡬνν ν¨μλ‘, λ κ° μ¬μ΄λ₯Ό λΆλλ½κ² λ³νμν¬λ μ¬μ©ν©λλ€.
progressμ νμ¬ κ°μ νκ² κ°μΌλ‘ λΊ κ°μ Math.absλ₯Ό μ¬μ©νμ¬ μ λ κ°μΌλ‘ λ§λ€κ³ , μ΄ κ°μ΄ 0.02λ³΄λ€ μμλλ strengthμ νκ² κ°μ 0μΌλ‘, ν΄ μμλ strengthμ κΈ°λ³Έ κ°μΈ -0.8λ‘ μ€μ (μ€ν¬λ‘€ νμ§ μμ μμλ λ¬Όκ²° ν¨κ³Όλ₯Ό μ£Όμ§ μκΈ° μν΄μ)
uniformμ uProgress κ°μ progressμ νμ¬ κ°μΌλ‘, uStrength κ°μ strengthμ νμ¬ κ°μΌλ‘ μ€μ νκΈ°
function setMesh(i) { ... const material = new THREE.ShaderMaterial({ uniforms: { uTexture: new THREE.Uniform(textures[i]), uResolution: uniforms.uResolution, uEffectWidth: uniforms.uEffectWidth, uProgress: uniforms.uProgress, uStrength: uniforms.uStrength, }, vertexShader: ` uniform float uProgress; uniform float uEffectWidth; uniform float uStrength; uniform vec2 uResolution; varying vec2 vUv; void main(){ vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0); float normalizedX = modelViewPosition.x / uResolution.x; normalizedX = (normalizedX - uProgress) / (uEffectWidth * 1.2); float mappedVar = normalizedX * 2.0; float wave = abs(normalizedX - mappedVar); wave = smoothstep(0.0, 0.5, wave); wave = 1.0 - wave; modelViewPosition.z += wave * uStrength * 100.0; gl_Position = projectionMatrix * modelViewPosition; vUv = uv; } `, ... , }); ... }
ShaderMaterialμ κ° uniform μμλ€ μΆκ°νκΈ°uniforms: { uTexture: new THREE.Uniform(textures[i]), // TextureLoaderλ₯Ό μ¬μ©νμ¬ λΆλ¬μ¨ ν μ€μ³ μ μ₯ uResolution: uniforms.uResolution, // νμ¬ νλ©΄μ ν΄μλ uEffectWidth: uniforms.uEffectWidth, // λ¬Όκ²° ν¨κ³Όμ λλΉ uProgress: uniforms.uProgress, // μ€ν¬λ‘€ μ§νλ uStrength: uniforms.uStrength, // λ¬Όκ²° ν¨κ³Όμ κ°λ },
vertexShader μμ μ μ₯ν΄ λ κ° uniform κ° λΆλ¬μ€κΈ°float: 3.14, 0.5 // λΆλμμμ μ«μ int: 1, -10 // μ μ bool: true, false // λ Όλ¦¬κ°, λΆλ¦¬μΈ vec2: vec2(0.5, 1.0) // 2μ°¨μ λ²‘ν° vec3: vec3(1.0, 0.0, 0.0) // 3μ°¨μ λ²‘ν° vec4: vec4(1.0, 0.0, 0.0, 1.0) // 4μ°¨μ λ²‘ν° mat2, mat3, mat4: mat4(1.0) // νλ ¬ λ°μ΄ν° νμ sampler2D: texture(sampler, uv) // 2D ν μ€μ² μνλ§ (μ£Όλ‘ ν μ€μ³ λΆλ¬μ¬ λμ μ¬μ©)
λͺ¨λΈ μ’νλ₯Ό μΉ΄λ©λΌ κΈ°μ€ μ’νλ‘ λ³νν κ°μΈ modelViewMatrixμ νμ¬ νλ μΈμ μ’ν κ°μΈ positionμ κ³±νμ¬ vec4 λ³μ modelViewPosition λ§λ€κΈ° (μΉ΄λ©λΌ κΈ°μ€ μ’νλ‘ μμ
νμ¬ λ¬Όκ²° ν¨κ³Όλ₯Ό μΌκ΄μ± μκ² μ μ©μν€κΈ° μν΄μ)
modelViewPositionμ xκ°μ νμ¬ ν΄μλμΈ uResolution.xμΌλ‘ λλ ν float λ³μ normalizedXμ μ μ₯ (ν΄μλμ λ°λ₯Έ μ κ·ν κ°, κ°μ 0μμ 1 λ²μλ‘ λ³ννκΈ° μν΄μ)
μ€ν¬λ‘€μ μ§νλ (uProgress)μ, λ¬Όκ²°ν¨κ³Όμ λλΉ (uEffectWidth)μ λ°λΌ normalizedX κ° μ‘°μ νκΈ°
normalizedX = (normalizedX - uProgress) // μ€ν¬λ‘€ ν λ λ¬Όκ²° ν¨κ³Όμ λ°©ν₯ μ€μ (μΌμͺ½μμ μ€λ₯Έμͺ½μΌλ‘) / (uEffectWidth * 1.2); // λ¬Όκ²° ν¨κ³Όμ λλΉμ μμμ κ° κ³±νκΈ° (μ μμ μμλ 1.2 μ¬μ©)
μ‘°μ λ normalizedX κ°μ 2.0μ κ³±νμ¬ float λ³μ mappedVar λ§λ€κΈ° (μ μμ μμλ 2.0 μ¬μ©)
normalizedXμμ mappedVarμ μ°¨μ΄ κ°μ abs ν¨μλ₯Ό μ¬μ©νμ¬ μ λκ°μΌλ‘ λ§λ€κ³ float λ³μ waveμ μ μ₯ (λ κ°μ μ°¨μ΄λ₯Ό λ¬Όκ²° ν¨κ³Όμ μ¬μ©νκΈ° μν΄μ)
smoothstep ν¨μλ₯Ό μ¬μ©νμ¬ λ¬Όκ²° ν¨κ³Όλ₯Ό λΆλλ½κ² λ§λ€κΈ° (0.0λΆν° 0.5 μ¬μ΄μ κ°μ κ²½κ³κ°μΌλ‘ μ¬μ©)
float smoothstep(float edge0, float edge1, float x); // edge0: 보κ°μ΄ μμλλ κ° (μ΅μ κ²½κ³κ°) // edge1: 보κ°μ΄ λλλ κ° (μ΅λ κ²½κ³κ°) // x: λ³΄κ° λμ κ° // λ κ²½κ³κ° μ¬μ΄μμ λ§€λλ½κ² 보κ°(smooth interpolation)μ μννλ μν μ ν©λλ€.
wave κ° λ°μ μν€κΈ°
wave = 1.0 - wave; // λ¬Όκ²° ν¨κ³Όλ₯Ό μ€μ¬ λΆλΆμμ κ°μ₯ κ°νκ² λ§λ€κΈ° μν΄μ
modelViewPositionμ zκ° μ‘°μ νκΈ°
modelViewPosition.z += wave * uStrength * 100.0; // λ³μ waveμ λ¬Όκ²° ν¨κ³Όμ κ°λ(uStrength)μ μμμ κ°(100.0)μ κ³±ν νμ modelViewPositionμ z μμΉμ λνκΈ°
projectionMatrixλ₯Ό μ‘°μ λ modelViewPositionμ κ³±νμ¬ μ΅μ’
μμΉ (gl_Position) μ€μ
uvμ’νλ₯Ό fragmentShaderμμ μ¬μ©νκΈ° μν΄, varying λ³μ vUv λ§λ€κΈ° (uv μ’νλ attribute μμ±μ΄κΈ° λλ¬Έμ fragmentShaderμμ μ¬μ© λΆκ°)
const material = new THREE.ShaderMaterial({ uniforms: { uTexture: new THREE.Uniform(textures[i]), .., } , fragmentShader: ` uniform sampler2D uTexture; varying vec2 vUv; void main(){ vec4 tex = texture2D(uTexture, vUv); gl_FragColor = vec4(tex.rgb, 1.0); } ` });
sampler2Dλ₯Ό μ¬μ©νμ¬ κ° ν
μ€μ³(uTexture) λΆλ¬μ€κΈ°
vertexShaderμμ μ μ₯ν vUv κ° (varying) λΆλ¬μ€κΈ°
texture2Dλ‘ ν
μ€μ³ λ° uv κ°μ λ³ννμ¬ vec4 λ³μ texμ μ μ₯νκΈ°
λ³μ texμ rgbμ νμ©νμ¬ νλ μΈμ μ΅μ’
μμ μ€μ (1.0μ νλ μΈμ ν¬λͺ
λ alpha κ°)
* rgbλ xyzμ κ°μ΅λλ€. (μμμ΄κΈ° λλ¬Έμ rgbλ‘ μ¬μ©)