Closed3

Pyhtonウェブアプリを作る:Three.js の復習

Junji FujimotoJunji Fujimoto

今回のスクラップはフロントエンドの実装部分でThree.jsを使いたいのでその復習.

やりたいこと

WebGLをjavascriptで簡単に扱えるライブラリThree.jsを復習する.

前提

Pythonのウェブアプリ開発フレームワークFlaskを使っている.

Three.js のCDNを使ってみる

以前はnpmを使っていたが今回は簡単に使いたいのでHTMLに数行書き込むだけで使えるCDNを使わせてもらう.

layout.html
<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">

        <script type="importmap">
            {
                "imports": {
                    "Three": "https://cdn.skypack.dev/three@0.128.0",
                    "OrbitContoles": "https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js",
                    "DragContoles": "https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/DragControls.js",
                    "BufferGeometryUtils": "https://cdn.skypack.dev/three@0.128.0/examples/jsm/utils/BufferGeometryUtils.js"
                }
            }
        </script>

        <script type="module" src="{{ url_for('static', filename='js/init.js') }}"></script>

        <link rel="stylesheet" href="{{ url_for('static', filename='css/stylesheet.css') }}">

        <title>{{ title }}</title>

        {% block head%}{% endblock %}
    </head>
    <body>
    {% block content %}
    {% endblock %}
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
    </body>
</html>
index.html
{% extends "layout.html" %}
{% block content %}
<h1>vis-g</h1>
{{ name }}
<body>
  <canvas id="canvas"></canvas>
</body>
{% endblock %}
init.js
import * as THREE from 'Three';
import { OrbitControls } from 'OrbitContoles';
import { BufferGeometryUtils } from 'BufferGeometryUtils';

export let renderer, scene, camera, controls;
export let div = document.querySelector("#canvas");
export let buttons = {};

window.addEventListener('DOMContentLoaded', init);

function init() {
    const canvasElement = div.querySelector("canvas");
    const width = canvasElement.offsetWidth;
    const height = canvasElement.offsetHeight;

    // レンダラーを作成
    renderer = new THREE.WebGLRenderer({
        canvas: canvasElement,
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setClearColor(0xffffff, 1.0);
    renderer.setSize(width, height);

    // シーンを作成
    scene = new THREE.Scene();

    // カメラを作成
    camera = new THREE.PerspectiveCamera(
        45,
        width / height,
        1,
        100
    );
    camera.position.set(25, 25, 25);

    // XYZ軸
    setCoordinate(10); // length: 10

    // カメラコントローラーを作成
    controls = new OrbitControls(camera, renderer.domElement);
    //
    controls.maxPolarAngle = Math.PI * 0.5;
    controls.minDistance = 0.1;
    controls.maxDistance = 100;
    // カメラの自動回転設定
    // 滑らかにカメラコントローラーを制御する
    controls.enableDamping = true;
    controls.dampingFactor = 0.2;
    controls.enabled = true;
    buttons['OrbitControls'] = makeSwitchButton(div, 'oc', 'ぐりぐり', controls);

    // 平行光源
    const directionalLight = new THREE.DirectionalLight(
        0xffffff
    );
    directionalLight.position.set(1, 1, 1);
    // シーンに追加
    scene.add(directionalLight);

    // 初回実行
    renderer.render(scene, camera);

    function setCoordinate(h) { // {{{
        const r = 0.1;
        const origin = [0, 0, 0];
        const directions = [ [h, 0, 0], [0, h, 0], [0, 0, h]];
        const rotations  = [ [0, 0, -h], [0, h, 0], [h, 0, 0]];
        const colors = [new THREE.Color("#ff0000"), new THREE.Color("#00ff00"), new THREE.Color("#0000ff")];

        for (let i = 0; i < 3; i++) {
            const axes = [];

            const material = new THREE.MeshPhongMaterial({color: colors[i]});

            const axis = new THREE.Vector3(rotations[i][0],rotations[i][1],rotations[i][2]).normalize();
            const q = new THREE.Quaternion();
            q.setFromAxisAngle(axis, 0.5*Math.PI);

            const geometryArrow = makeArrowGeometry(r, h);
            geometryArrow.applyMatrix4(new THREE.Matrix4().makeRotationFromQuaternion(q));;
            const mesh = new THREE.Mesh(geometryArrow, material);

            scene.add(mesh);
        }; // }}}
    };
};

function makeArrowGeometry(r, h) {
    const axes = [];
    const n = 10;

    const geometryCylinder = new THREE.CylinderGeometry(r, r, h*0.8, 20, n*h, false);
    geometryCylinder.translate(0, h*0.4, 0);
    axes.push(geometryCylinder);

    const geometryCone = new THREE.CylinderGeometry(0, r*5, h*0.2, 20, n, false);
    geometryCone.translate(0, h*0.9, 0);
    axes.push(geometryCone);

    return BufferGeometryUtils.mergeBufferGeometries(axes);
};

export function makeSwitchButton(div, id, str, obj) {
    let html = document.createElement('input');
    html.type = "button";
    html.value = str + ": " + ((obj.enabled) ? "on" : "off");
    html.addEventListener('click', function(e){
        obj.enabled = !obj.enabled;
        html.value = str + ": " + ((obj.enabled) ? "on" : "off");
    });
    div.appendChild(html);

    return html;
};
index.js
import * as THREE from 'Three';
import { DragControls } from 'DragContoles';

import { renderer, scene, camera, controls, div, buttons, makeSwitchButton } from './init.js';

window.addEventListener('DOMContentLoaded', index);

function index() {
//    let n = 2;
//    const cube = new THREE.Mesh(
//        new THREE.BoxGeometry(n, n, n),
//        new THREE.MeshNormalMaterial());
//    cube.position.set(n*0.5,n*0.5,n*0.5);
//    scene.add(cube);

    const geometry = new THREE.BoxGeometry()

    const material = [
        new THREE.MeshPhongMaterial({ color: 0xff0000, transparent: true }),
        new THREE.MeshPhongMaterial({ color: 0x00ff00, transparent: true }),
        new THREE.MeshPhongMaterial({ color: 0x0000ff, transparent: true })
    ];

    const cubes = [
        new THREE.Mesh(geometry, material[0]),
        new THREE.Mesh(geometry, material[1]),
        new THREE.Mesh(geometry, material[2])
    ];

    const cubes2 = [
        new THREE.Mesh(geometry, material[0]),
        new THREE.Mesh(geometry, material[1]),
        new THREE.Mesh(geometry, material[2])
    ];

    cubes[0].position.x = -2;
    cubes[1].position.x = 0;
    cubes[2].position.x = 2;
    cubes.forEach((c) => scene.add(c));

    cubes2.forEach((c) => scene.add(c));

    var dragcontrols = new DragControls(cubes, camera, renderer.domElement);
    buttons['DragControls'] = makeSwitchButton(div, 'dc', 'うごかす', dragcontrols);

    update();

    function render() {
//        cube.position.x += 0.01;
//        cube.rotation.x += 0.01;
        cubes2[0].position.x = -cubes[0].position.x;
        cubes2[0].position.y = -cubes[0].position.y;
        cubes2[0].position.z = -cubes[0].position.z;
        renderer.render(scene, camera);
    };

    function update() {
        // レンダリング
        render();
        requestAnimationFrame(update);
    };
};

参考:

このスクラップは2022/07/31にクローズされました