【Three.js】マウスに追従するライト
なかなか良い雰囲気
はじめに
マウスの周りだけボンヤリ明るい。みたいなモノを作ってみましょう。
できた
デモサイトはこちら。
簡単にできるかなぁと思ったんですけど、意外と手こずりましたね。そこを重点的に解説していこうと思います。
座標を変換しないといけない
ディスプレイ上のマウスの座標はコレで取得できます↓。
document.body.addEventListener("mousemove", function(e) {
var dispX = e.clientX;
var dispY = e.clientY;
});
これをライトの"position.x"と"position.y"に入れてあげればいい。とか思っちゃいそうだけどそんな甘くない。なぜならライトは3D空間上のモノだから。
だからこのディスプレイ上の座標を3D空間の座標に変換してあげる必要があります。
まずは座標を-1~1の間の値に変換する
まず気を付けないといけない事は、ディスプレイと3D空間とでは原点の位置が違うということ。ディスプレイは左上が原点で、3Dは真ん中。そこも意識しつつ、ディスプレイ上の座標を-1~1の間に変換します。それがコレ↓。
// ディスプレイ上でのマウスの座標(-1~1に変換)
var dispX = (e.clientX / window.innerWidth) * 2 - 1;
var dispY = -(e.clientY / window.innerHeight) * 2 + 1;
言葉で説明するのがムズカシイので絵にしました。X座標が40の時の例です。
とてもよく分かりましたね。で、これで求めたdispXに3D空間の幅を、dispYに3D空間の高さをかけてあげれば、3D空間上の座標に変換できるというワケですね。大丈夫。よく考えれば分かるハズです。
3D空間の幅と高さを求める
3D空間の幅と高さは、カメラからの距離とカメラの視野角から求めることができます↓。
// 3D空間の高さと幅
var heightOnOrigin = (Math.tan(((fov * Math.PI / 180) / 2)) * (camera.position.z - plZ) * 2)
var widthOnOrigin = heightOnOrigin * aspRatio
計算式がゴチャゴチャして分かりづらいですね。絵にしました。ライトが動く面の高さを求めています。
幅は「高さ×カメラのアスペクト比」です。
この高さと幅の2分の1を、さっき求めた-1~1の値にかけてあげれば、3D空間上の座標がでます。
// ライトの座標
pointLight.position.set(dispX * widthOnOrigin / 2, dispY * heightOnOrigin / 2, plZ);
なぜ2分の1なのかと言うと、3D空間の原点は真ん中だからです。これもがんばれば分かります。
全コードを載せておきます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>妖怪FILE</title>
<script type="text/javascript" src="../libs/three.js"></script>
<style>
html, body {
margin: 0;
overflow: hidden;
height: 100%;
}
#screen {
height: 100%;
}
</style>
</head>
<body>
<div id="screen"></div>
<script type="text/javascript">
window.onload = function() {
// シーン
var scene = new THREE.Scene();
/***** カメラの設定 *****/
// アスペクト比
var aspRatio = window.innerWidth / window.innerHeight;
// 視野角
var fov;
if (aspRatio > 1) {
fov = 35;
} else if (aspRatio > 0.9) {
fov = 45
} else if (aspRatio > 0.8) {
fov = 50;
} else if (aspRatio > 0.6) {
fov = 60;
} else if (aspRatio > 0.5) {
fov = 70;
} else {
fov = 80;
}
var camera = new THREE.PerspectiveCamera(fov, aspRatio, 0.1, 1000);
camera.position.z = 20;
camera.lookAt(scene.position);
// レンダラー
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById("screen").appendChild(renderer.domElement);
// ライト
var pointLight = new THREE.PointLight(0xffffff);
pointLight.position.set(100, 100, 100);
pointLight.decay = 1;
pointLight.intensity = 4;
pointLight.distance = 20;
pointLight.visible = false;
scene.add(pointLight);
// テクスチャ
var textureLoader = new THREE.TextureLoader();
var texture = textureLoader.load("../assets/textures/yokai12.jpg");
// パネル
var panelGeometry = new THREE.PlaneGeometry(14, 10.5, 1, 1);
var panelMaterial = new THREE.MeshPhongMaterial();
panelMaterial.map = texture;
var panel = new THREE.Mesh(panelGeometry, panelMaterial);
scene.add(panel);
/***** 描画関数 *****/
function renderScene() {
renderer.render(scene, camera);
requestAnimationFrame(renderScene);
}
/***** スマホ用ライトの位置設定関数 *****/
function setLightPositionForSP(e) {
// ライトのZ座標
const plZ = 0.1;
// ディスプレイ上でのタッチの座標(-1~1に変換)
var dispX = (e.changedTouches[0].clientX / window.innerWidth) * 2 - 1;
var dispY = -(e.changedTouches[0].clientY / window.innerHeight) * 2 + 1;
// 3D空間の高さと幅
var heightOnOrigin = (Math.tan(((fov * Math.PI / 180) / 2)) * (camera.position.z - plZ) * 2)
var widthOnOrigin = heightOnOrigin * aspRatio
// ライトの座標を設定
pointLight.position.set(dispX * widthOnOrigin / 2, dispY * heightOnOrigin / 2, plZ);
}
/***************************************/
/***** ライトの座標を設定 ここから *****/
/***************************************/
/***** PC用 *****/
document.body.addEventListener("mousemove", function(e) {
pointLight.visible = true;
// ライトのZ座標
const plZ = 0.1;
// ディスプレイ上でのマウスの座標(-1~1に変換)
var dispX = (e.clientX / window.innerWidth) * 2 - 1;
var dispY = -(e.clientY / window.innerHeight) * 2 + 1;
// 3D空間の高さと幅
var heightOnOrigin = (Math.tan(((fov * Math.PI / 180) / 2)) * (camera.position.z - plZ) * 2)
var widthOnOrigin = heightOnOrigin * aspRatio
// ライトの座標
pointLight.position.set(dispX * widthOnOrigin / 2, dispY * heightOnOrigin / 2, plZ);
});
/***** スマホ用 *****/
// 触った時
document.body.addEventListener("touchstart", function(e) {
pointLight.intensity = 8;
pointLight.distance = 40;
pointLight.visible = true;
setLightPositionForSP(e);
});
// 動かしている時
document.body.addEventListener("touchmove", function(e) {
setLightPositionForSP(e);
});
// 離した時
document.body.addEventListener("touchend", function(e) {
pointLight.intensity = 4;
pointLight.distance = 20;
pointLight.visible = false;
});
/***************************************/
/***** ライトの座標を設定 ここまで *****/
/***************************************/
/***** ウィンドウサイズ変更 *****/
window.addEventListener("resize", function(){
// アスペクト比
aspRatio = window.innerWidth / window.innerHeight;
// 視野角
if (aspRatio > 1) {
fov = 35;
} else if (aspRatio > 0.9) {
fov = 45
} else if (aspRatio > 0.8) {
fov = 50;
} else if (aspRatio > 0.6) {
fov = 60;
} else if (aspRatio > 0.5) {
fov = 70;
} else {
fov = 80;
}
camera.aspect = aspRatio;
camera.fov = fov;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
render();
});
// 描画
renderScene();
}
</script>
</body>
</html>
あとがき
おばけ屋敷の特設サイトとかでどうでしょう。見づらいけど。
座標の変換が大変でしたが何とかできました。ライトが動く面の高さと幅を求めるとこなんか、自分でもよくできてるなぁと思います。はい。
でも説明がねぇ…。ヘタクソだなぁ。そこら辺の工夫が必要ですね。ありがとうございました。
おしゃれ度
★★★☆☆
ディスカッション
コメント一覧
まだ、コメントがありません