概要
マウスを中心に魚眼レンズのように頂点座標が移動するVertex Shaderの実装方法をまとめました。
React Three Fiber(Three.jsのReact用ラッパーパッケージ)を使用しています。
実装
実装の全容は、【CodeSandbox】のリンクを確認してください。
ここでは、Shaderに関係するコードの設計手順を説明します。
イメージを作る
どんなコーディングでもそうですが、実装に入る前にどんなものを実装したくて、それには技術的に何が必要なのかを整理します。
今回の場合だと、作りたいものは、魚眼レンズのようにマウスを中心に頂点座標が移動するShaderです。
そして、それを実現させるためには大きく分けて2つ必要な技術があります。
- マウスがMesh上にあるとき、その座標を取得する。
- マウス座標を中心に頂点を移動させる。マウス座標に近い頂点ほど大きく移動させる。
これをイメージにしたものが、以下になります。
Mesh(Plane)上にマウスがあります。例えば、このときの "●" と、 "●" の頂点の動きを考えます。
コード
/* eslint-disable @typescript-eslint/no-use-before-define */
import { gsap, Linear } from 'gsap'
import { folder, useControls } from 'leva'
import React, { useMemo, VFC } from 'react'
import * as THREE from 'three'
import { ThreeEvent } from '@react-three/fiber'
export const ShaderObject: VFC = () => {
// ----------------------------
// controller
const datas = useControls({
geometry: { options: ['plane', 'sphere', 'cube'] },
inner: true,
uniforms: folder({
range: { value: 0.2, min: 0.1, max: 1, step: 0.1 },
scale: { value: 0.1, min: 0, max: 0.2, step: 0.01 }
})
})
// ----------------------------
// mesh relation
const material = useMemo(
() =>
new THREE.ShaderMaterial({
uniforms: {
u_mouse: { value: [0, 0, 0] },
u_range: { value: datas.range },
u_scale: { value: 0 }
},
vertexShader: vertexShader,
fragmentShader: fragmentShader,
wireframe: true
}),
[datas.range]
)
const geometry = useMemo(() => {
let geo: THREE.BufferGeometry
switch (datas.geometry) {
case 'plane':
geo = new THREE.PlaneGeometry(1, 1, 20, 20)
break
case 'sphere':
geo = new THREE.IcosahedronGeometry(0.5, 10)
break
case 'cube':
geo = new THREE.BoxGeometry(1, 1, 1, 20, 20, 20)
break
default:
geo = new THREE.PlaneGeometry(1, 1, 20, 20)
}
return geo
}, [datas.geometry])
// ----------------------------
// event
const handleClick = (e: ThreeEvent<MouseEvent>) => {
// debug
console.log('point', e.point)
console.log('normal', e.face?.normal)
}
const handlePointerMove = (e: ThreeEvent<PointerEvent>) => {
material.uniforms.u_scale.value = datas.scale
material.uniforms.u_mouse.value = e.point
}
const handlePointerLeave = (e: ThreeEvent<PointerEvent>) => {
const gsapOptions: gsap.TweenVars = {
ease: Linear.easeOut,
duration: 0.2
}
gsap.to(material.uniforms.u_scale, { value: 0, ...gsapOptions })
}
return (
<>
<mesh
geometry={geometry}
material={material}
onPointerMove={handlePointerMove}
onPointerLeave={handlePointerLeave}
// onClick={handleClick}
/>
{datas.inner && (datas.geometry === 'sphere' || datas.geometry === 'cube') && (
<mesh geometry={geometry} scale={0.98}>
<meshBasicMaterial color="#000" />
</mesh>
)}
</>
)
}
// ====================================================
// shader
const vertexShader = `
uniform vec3 u_mouse;
uniform float u_range;
uniform float u_scale;
varying vec3 v_position;
void main() {
vec3 pos = position;
if (0.0 < u_scale){
float dist = distance(u_mouse, pos);
dist = clamp(dist, 0.0, u_range);
vec3 vecDir = normalize(pos - u_mouse);
pos += vecDir * (u_range - dist) / u_range * u_scale;
}
v_position = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`
const fragmentShader = `
varying vec3 v_position;
void main() {
vec3 color = v_position + 0.5;
gl_FragColor = vec4(color, 1.0);
}
`
マウス座標の取得
React Three Fiberを使用すると、マウスがMeshにあたっているときの座標はとても簡単に取得できます。
const handleClick = (e: ThreeEvent<MouseEvent>) => {
// debug
console.log('point', e.point)
console.log('normal', e.face?.normal)
}
このコードは、デバッグ用に書いているコードですが、Meshをクリックしたときに、クリックした位置の座標と法線ベクトルを取得しています。
同様に、Mesh上をマウスが移動するとき(handlePointerMove
)でも座標を取得できます。
Shader
イメージはできているので、あとはそれをコードで表すだけです。
const vertexShader = `
uniform vec3 u_mouse;
uniform float u_range;
uniform float u_scale;
varying vec3 v_position;
void main() {
vec3 pos = position;
if (0.0 < u_scale){
float dist = distance(u_mouse, pos);
dist = clamp(dist, 0.0, u_range);
vec3 vecDir = normalize(pos - u_mouse);
pos += vecDir * (u_range - dist) / u_range * u_scale;
}
v_position = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`
変数は、以下のようになっています。
u_mouse
:Mesh上のマウス座標
u_range
:影響範囲の半径(範囲内の頂点しか移動しないようにする)
u_scale
:どれくらい拡大するか
v_position
:Fragment Shaderに渡して、色を付けるために使っているので特に触れません
まず、if (0.0 < u_scale)
で、スケールが0より大きいときだけ処理を行います。これは、マウスがMeshから離れたときに処理を行わないようにするための判定です。
handlePointerLeave
は、Meshからマウスが離れたときに発生するイベントで、ここでスケールを0に設定しています。
const handlePointerLeave = (e: ThreeEvent<PointerEvent>) => {
const gsapOptions: gsap.TweenVars = {
ease: Linear.easeOut,
duration: 0.2
}
gsap.to(material.uniforms.u_scale, { value: 0, ...gsapOptions })
}
さらに、gsapを使用して、マウスが離れても移動した頂点が元の位置に一瞬で戻らないように、アニメーションさせています。
頂点の座標を移動する処理は、以下のようになっています。
float dist = distance(u_mouse, pos);
dist = clamp(dist, 0.0, u_range);
vec3 vecDir = normalize(pos - u_mouse);
pos += vecDir * (u_range - dist) / u_range * u_scale;
-
distance(u_mouse, pos)
では、マウス座標と対象の頂点座標の距離を取っています。 -
clamp(dist, 0.0, u_range)
では、距離が収まる範囲を制限しています。(距離は、0 ~ u_range
以内) -
normalize(pos - u_mouse)
では、マウスから頂点のベクトルを求めて、それを単位ベクトルに基準化しています。基準化しているのは、ベクトルの方向成分だけが欲しいからです。(ベクトルの大きさはいらいない) -
pos += vecDir * (u_range - dist) / u_range * u_scale
では、頂点座標をマウスからのベクトル方向(vecDir
)に移動しています。その移動量は、(u_range - dist) / u_range
で頂点がマウスに近いほど1、遠いほど0になり、u_scale
を掛けて倍率を決めています。
色々なGeometryで試してみる
Shader内で頂点位置の移動に使用しているのは、マウス座標と頂点座標だったので、このShaderは形状によらず使うことができます。
Textureを貼ってみる
PlaneにTextureを貼ると、本当に魚眼レンズのようになります。
CodeSandbox
まとめ
VertexShaderを扱うには慣れが必要ですが、慣れれば面白い表現が色々できそうです