Three.jsで積分領域を可視化する (THREE.LatheBufferGeometryを使って回転体を作る)

## 序章
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$ 軸回りの回転体を形成するため(回転軸を指定することができない), 適宜座標を入れ替える必要がある.


## アニメーション

パラメータを動かすことで, 回転体が形成される様子を見ることができる.

![kyoto2007b2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/668922/e560c01b-7a83-2e56-69e2-34fd41cc88c7.gif)


## 実装

```JavaScript
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, オライリージャパン.