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λ‘ μ¬μ©)