/**
* @fileOverview Three.js で簡単なブラウザアプリを作るユーティリティ。
* @author K.Miyawaki
*/
/**
* 演習用関数群の名前空間
* @namespace
*/
var mylib2020 = mylib2020 || {};
/**
* 物体前方を表すベクトル。
* @type {THREE.Vector3}
*/
mylib2020.FORWARD = new THREE.Vector3(0, 0, 1);
/**
* 物体後方を表すベクトル。
* @type {THREE.Vector3}
*/
mylib2020.BACK = new THREE.Vector3(0, 0, -1);
mylib2020.createRectSize = function (x, y, w, h) {
return { x: x, y: y, width: w, height: h };
}
mylib2020.calcScreenSize = function (aspect, viewPortWidth, viewPortHeight) {
let w = viewPortWidth
let h = viewPortHeight;
if (h * aspect > w) {
h = w / aspect;
} else {
w = h * aspect;
}
return mylib2020.createRectSize(0, 0, w, h);
}
/**
* Three.js を初期化し、シーンを生成する。
* @param {number} width シーンの横画素数。
* @param {number} height シーンの縦画素数。
* @param {Object} opts 生成のオプション。次のようなキーでパラメータ指定する。null のとき、デフォルト値が使用される。
* <ul>
* <li>fov - number 画角。(デフォルト: 45.0)</li>
* <li>near - number カメラのどのくらい近くから描画範囲に含めるか。(デフォルト: 0.1)</li>
* <li>far - number カメラのどのくらい遠くまで描画範囲に含めるか。(デフォルト: 1000)</li>
* <li>axesLength - number シーンに表示するワールド座標軸の長さ。(デフォルト: 20)</li>
* <li>clearColor - number シーンの何もない領域を塗りつぶす色。(デフォルト: 0x222222)</li>
* <li>camPosX - number カメラの初期位置。(デフォルト: 0)</li>
* <li>camPosY - number カメラの初期位置。(デフォルト: 2)</li>
* <li>camPosZ - number カメラの初期位置。(デフォルト: -7)</li>
* </ul>
* @returns {Array} 次の要素が入った配列。
* <ul>
* <li>THREE.Scene</li>
* <li>THREE.PerspectiveCamera</li>
* <li>THREE.WebGLRenderer</li>
* <li>THREE.Clock</li>
* <li>THREE.AxesHelper (opts.axesLength が 0 以下の場合は生成されず、 null が帰る)</li>
* </ul>
*/
mylib2020.initThree = function (width, height, opts) {
opts = opts || {};
const aspect = width / height;
const fov = ('fov' in opts) ? opts.fov : 45.0;
const near = ('near' in opts) ? opts.near : 0.1;
const far = ('far' in opts) ? opts.far : 1000;
const axesLength = ('axesLength' in opts) ? opts.axesLength : 20;
const clearColor = ('clearColor' in opts) ? opts.clearColor : 0x222222;
const camPosX = ('camPosX' in opts) ? opts.camPosX : 0;
const camPosY = ('camPosY' in opts) ? opts.camPosY : 2;
const camPosZ = ('camPosZ' in opts) ? opts.camPosZ : -7;
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
let renderer = new THREE.WebGLRenderer({ antialias: true });
let clock = new THREE.Clock();
let axes = null;
if (axesLength > 0) {
axes = new THREE.AxesHelper(axesLength);
scene.add(axes);
}
renderer.setClearColor(new THREE.Color(clearColor));
renderer.setSize(width, height);
renderer.shadowMap.enabled = true;
camera.position.x = camPosX;
camera.position.y = camPosY;
camera.position.z = camPosZ;
camera.lookAt(new THREE.Vector3(0, -0.5, 0));
return [scene, camera, renderer, clock, axes];
}
/**
* Three.js を初期化し、シーンを生成する。
* @param {HTMLElement} element シーンの親となる HTML 要素。
* @param {Object} opts 生成のオプション。次のようなキーでパラメータ指定する。null のとき、デフォルト値が使用される。
* <ul>
* <li>fov - number 画角。(デフォルト: 45.0)</li>
* <li>near - number カメラのどのくらい近くから描画範囲に含めるか。(デフォルト: 0.1)</li>
* <li>far - number カメラのどのくらい遠くまで描画範囲に含めるか。(デフォルト: 1000)</li>
* <li>axesLength - number シーンに表示するワールド座標軸の長さ。(デフォルト: 20)</li>
* <li>clearColor - number シーンの何もない領域を塗りつぶす色。(デフォルト: 0x222222)</li>
* <li>camPosX - number カメラの初期位置。(デフォルト: 0)</li>
* <li>camPosY - number カメラの初期位置。(デフォルト: 2)</li>
* <li>camPosZ - number カメラの初期位置。(デフォルト: -7)</li>
* </ul>
* @returns {Array} 次の要素が入った配列。
* <ul>
* <li>THREE.Scene</li>
* <li>THREE.PerspectiveCamera</li>
* <li>THREE.WebGLRenderer</li>
* <li>THREE.Clock</li>
* <li>THREE.AxesHelper (opts.axesLength が 0 以下の場合は生成されず、 null が帰る)</li>
* </ul>
*/
mylib2020.initThreeInElement = function (element, opts) {
const rect = element.getBoundingClientRect();
let [scene, camera, renderer, clock, axes] = mylib2020.initThree(rect.width, rect.height, opts);
element.appendChild(renderer.domElement);
return [scene, camera, renderer, clock, axes];
}
/**
* fromObject の中心から指定した方向にレイを飛ばし、 targetMeshes に含まれる物体と交叉するかどうかを判定する。<br/>
* 物理エンジンを用いない簡易な衝突判定法。
* @param {THREE.Object3D} fromObject レイの中心となる物体。
* @param {Array<THREE.Object3D>} targetMeshes レイの交叉判定対象となる物体群が入った配列。
* @param {THREE.Vector3} direction レイの方向。
* @param {number} [distance=1.5] レイとの交差地点と fromObject の距離がこの値未満なら衝突しているとみなす。
* @returns {boolean} 衝突している物体があるか否か。 true: 何かと衝突している。
*/
mylib2020.checkCollision = function (fromObject, targetMeshes, direction, distance = 1.5) {
const v = direction.clone();
v.applyEuler(fromObject.rotation);
const ray = new THREE.Raycaster(fromObject.position, v);
const objs = ray.intersectObjects(targetMeshes);
if (objs.length > 0) {
if (objs[0].distance < distance) {
return true;
}
}
return false;
}
mylib2020.initPushButton = function (element, activeColor, onPressed = null, onReleased = null, normalColor = null) {
const supportTouch = 'ontouchend' in document;
if (normalColor == null) {
normalColor = element.style.backgroundColor;
}
if (supportTouch) {
element.addEventListener('touchstart',
function (evt) {
this.style.backgroundColor = activeColor;
if (onPressed) {
onPressed(element);
}
},
{ passive: false });
element.addEventListener('touchend',
function (evt) {
this.style.backgroundColor = normalColor;
if (onReleased) {
onReleased(element);
}
},
{ passive: false });
} else {
element.addEventListener('mousedown', function (evt) {
this.style.backgroundColor = activeColor;
if (onPressed) {
onPressed(element);
}
});
element.addEventListener('mouseup', function (evt) {
this.style.backgroundColor = normalColor;
if (onReleased) {
onReleased(element);
}
});
}
}
/**
* キャラクタ操作用のデジタルな十字キーのボタンを生成する。
*/
mylib2020.ArrowButton = class {
/**
* @constructor
* @param {HTMLElement} container ボタンにしたい HTML 要素。
* @param {string} activeImage ボタンが押された場合に表示する画像 URL。
* @param {boolean} verbose デバッグメッセージを console.log で表示するか否か。
*/
constructor(container, activeImage, verbose = false) {
this.container = container;
this.normalImage = this.container.style.backgroundImage;
this.activeImage = activeImage;
this.supportTouch = 'ontouchend' in document;
this.verbose = verbose;
this.state = false;
const obj = this;
console.log("Touch support = " + this.supportTouch);
if (this.supportTouch) {
document.addEventListener('touchstart', function (evt) { obj.onTouchStart(evt); }, { passive: false });
document.addEventListener('touchend', function (evt) { obj.onTouchEnd(evt); }, { passive: false });
document.addEventListener('touchmove', function (evt) { obj.onTouchMove(evt); }, { passive: false });
} else {
this.container.addEventListener('mousedown', function (evt) { obj.onMouseDown(evt); });
this.container.addEventListener('mouseup', function (evt) { obj.onMouseUp(evt); });
this.container.addEventListener('mouseleave', function (evt) { obj.onMouseLeave(evt); });
}
this.update();
}
/**
* ボタンが押されているか否か。
* @returns {boolean} true: 押されている。 false: 押されていない。
*/
isPressed() {
return this.state;
}
update() {
if (this.isPressed()) {
this.container.style.backgroundImage = this.activeImage;
} else {
this.container.style.backgroundImage = this.normalImage;
}
}
press() {
this.state = true;
this.update();
}
release() {
this.state = false;
this.update();
}
outputLog(arg) {
if (this.verbose) {
console.log(arg);
}
}
contains(clientX, clientY, clientRect) {
if (clientX < clientRect.x || clientRect.x + clientRect.width < clientX) {
return false;
}
if (clientY < clientRect.y || clientRect.y + clientRect.height < clientY) {
return false;
}
return true;
}
// For Mobile Phone
checkTouch(e) {
let hasTouch = false;
let clientRect = this.container.getBoundingClientRect();
for (let touch of e.touches) {
this.outputLog(touch.clientX + "," + touch.clientY);
if (this.contains(touch.clientX, touch.clientY, clientRect)) {
hasTouch = true;
break;
}
}
if (hasTouch) {
e.preventDefault();
}
if (this.isPressed() == false) {
if (hasTouch) {
this.press();
}
} else if (hasTouch == false) {
this.release();
}
}
onTouchStart(e) {
this.outputLog('onTouchStart:');
this.checkTouch(e);
}
onTouchEnd(e) {
this.outputLog('onTouchEnd:');
this.checkTouch(e);
}
onTouchMove(e) {
this.outputLog('onTouchMove:');
this.checkTouch(e);
}
// For PC
onMouseDown(e) {
e.preventDefault();
this.outputLog('onMouseDown:');
this.press();
}
onMouseUp(e) {
e.preventDefault();
this.outputLog('onMouseUp:');
this.release();
}
onMouseLeave(e) {
e.preventDefault();
this.outputLog('onMouseLeave:');
this.release();
}
}