序章
Three.jsを利用することで, 比較的容易にWeb上で3Dモデルを作成することができる. 本稿では, Three.jsを用いて, 積分領域の可視化に取り組む.
今回扱う問題は, 回転体の体積を求める問題である. そこで, 回転体の形状を作成することを目指す.
数学的には, 断面の形状を求めてそれを円柱座標で書けば良い. Three.jsでは, THREE.LatheBufferGeometryを用いることで, 同様の発想で回転体を生成することができる.
正確には, 2次元領域の境界を点列(2次元ベクトルの配列)の形で与えることで, $y$軸回りの回転体を作ることができる.
さて, 以下が考える問題である.
問題
(京都大学2007文系2)
$3$次関数 $y = x^3 - 2 x^2 - x + 2$ のグラフ上の点 $(1, 0)$ における接線を $l$ とする. この$3$次関数のグラフと接線 $l$ で囲まれた部分を $x$ 軸の回りに回転して立体を作る. その立体の体積を求めよ.
方針
接線 $l$ は, $y = -2 x + 2 $ と書ける. また, $y = x^3 - 2 x^2 - x + 2$ と $y = -2 x + 2 $ は $ x = 0, 1$ で交わる.
それゆえ, $f(x) = x^3 - 2 x^2 - x + 2$ と $g(x) = -2 x + 2 $ について, $ x \in [0, 1]$ の範囲で頂点の組 $(x, f(x))$ と $(x, g(x))$ を生成し, そこからTHREE.LatheBufferGeometry を使って回転体を作ればよい.
なお, 点列の順番には意味があるので, 閉曲線を描くように点列を作成する必要がある.
また, THREE.LatheBufferGeometryは $y$ 軸回りの回転体を形成するため(回転軸を指定することができない), 適宜座標を入れ替える必要がある.
アニメーション
パラメータを動かすことで, 回転体が形成される様子を見ることができる.
実装
function init() {
let stats = initStats();
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
let webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000000));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMap.enabled = true;
camera.position.x = 30;
camera.position.y = 40;
camera.position.z = 50;
camera.lookAt(new THREE.Vector3(0, 0, 0));
const controler = new THREE.OrbitControls(camera);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);
const axisXLength = 20;
const axisXHeadLength = axisXLength * 0.05;
const axisXHeadWidth = axisXHeadLength * 0.5;
const directionX = new THREE.Vector3(1, 0, 0);
const startX = new THREE.Vector3(0, 0, 0);
const colorX = 0xff0000;
const axisX = new THREE.ArrowHelper(directionX, startX, axisXLength + axisXHeadLength * 2, colorX, axisXHeadaxisXHeadWidth);
scene.add(axisX);
const axisYLength = 20;
const axisYHeadLength = axisYLength * 0.05;
const axisYHeadWidth = axisYHeadLength * 0.5;
const directionY = new THREE.Vector3(0, 1, 0);
const startY = new THREE.Vector3(0, 0, 0);
const colorY = 0x00ff00;
const axisY = new THREE.ArrowHelper(directionY, startY, axisYLength + axisYHeadLength * 2, colorY, axisYHeadaxisYHeadWidth);
scene.add(axisY);
const axisZLength = 20;
const axisZHeadLength = axisZLength * 0.05;
const axisZHeadWidth = axisZHeadLength * 0.5;
const directionZ = new THREE.Vector3(0, 0, 1);
const startZ = new THREE.Vector3(0, 0, 0);
const colorZ = 0x00f6ff;
const axisZ = new THREE.ArrowHelper(directionZ, startZ, axisZLength + axisZHeadLength * 2, colorZ, axisZHeadaxisZHeadWidth);
scene.add(axisZ);
const fontLoader = new THREE.FontLoader();
fontLoader.load('../assets/fonts/helvetiker_bold.typeface.js', function (font) {
const textXGeometry = new THREE.TextGeometry('X', {
font: font,
size: axisXLength / 15,
height: 0,
curveSegments: 0,
bevelEnabled: true,
bevelThickness: 0,
bevelSize: 0,
bevelSegments: 0
});
const textXMaterial = new THREE.MeshBasicMaterial({ color: colorX });
const textX = new THREE.Mesh(textXGeometry, textXMaterial);
textX.position.set(axisXLength + axisXHeadLength * 2, 1, 0);
scene.add(textX);
const textYGeometry = new THREE.TextGeometry('Y', {
font: font,
size: axisYLength / 15,
height: 0,
curveSegments: 0,
bevelEnabled: true,
bevelThickness: 0,
bevelSize: 0,
bevelSegments: 0
});
const textYMaterial = new THREE.MeshBasicMaterial({ color: colorY });
const textY = new THREE.Mesh(textYGeometry, textYMaterial);
textY.position.set(1, axisYLength + axisYHeadLength * 2, 0);
scene.add(textY);
const textZGeometry = new THREE.TextGeometry('Z', {
font: font,
size: axisZLength / 15,
height: 0,
curveSegments: 0,
bevelEnabled: true,
bevelThickness: 0,
bevelSize: 0,
bevelSegments: 0
});
const textZMaterial = new THREE.MeshBasicMaterial({ color: colorZ });
const textZ = new THREE.Mesh(textZGeometry, textZMaterial);
textZ.position.set(-2, 1, axisZLength + axisZHeadLength * 2);
scene.add(textZ);
});
let latheMesh;
const phiStart = Math.PI / 2;
const actual_segments = 40;
let segments = actual_segments;
const start = 0;
const end = 1;
const pointNum = 15;
let controls = new function () {
this.phiLength = 2 * Math.PI;
this.scale = 10;
this.redraw = function () {
scene.remove(latheMesh);
segments = actual_segments * controls.phiLength / (2 * Math.PI);
generatePoints(segments, phiStart, controls.phiLength, controls.scale, start, end, pointNum);
};
};
generatePoints(segments, phiStart, controls.phiLength, controls.scale, start, end, pointNum);
let gui = new dat.GUI();
gui.add(controls, 'phiLength', 0, 2 * Math.PI).onChange(controls.redraw);
gui.add(controls, 'scale', 0.0, 20.0).onChange(controls.redraw);
render();
function generatePoints(segments, phiStart, phiLength, scale, start, end, pointNum) {
let points = [];
let x;
let y;
for (let i = 0; i <= pointNum; i++) {
x = (1 - i / pointNum) * start + (i / pointNum) * end;
y = g(x)
points.push(new THREE.Vector2(scale * y, - scale * x));
}
for (let i = pointNum; i >= 0; i--) {
x = (1 - i / pointNum) * start + (i / pointNum) * end;
y = f(x)
points.push(new THREE.Vector2(scale * y, - scale * x));
}
let latheGeometry = new THREE.LatheBufferGeometry(points, segments, phiStart, phiLength);
latheMesh = createMesh(latheGeometry);
latheMesh.rotation.z = Math.PI / 2;
scene.add(latheMesh);
}
function f(x) {
return x ** 3 - 2 * x ** 2 - x + 2;
}
function g(x) {
return -2 * x + 2;
}
function createMesh(geom) {
let facemat = [
new THREE.MeshBasicMaterial({ color: 0xffffff, opacity: 1, transparent: true }),
new THREE.MeshBasicMaterial({ color: 0xffffcf, side: THREE.BackSide, opacity: 1, transparent: true }
new THREE.MeshBasicMaterial({ color: 0x1f5e44, wireframe: true, wireframeLinewidth: 100.0 })
];
let mesh = THREE.SceneUtils.createMultiMaterialObject(geom, facemat);
return mesh;
}
function render() {
stats.update();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
function initStats() {
let stats = new Stats();
stats.setMode(0);
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
参考
・Jos Dirksen (あんどうやすし 訳), 初めてのThree.js, オライリージャパン.