【WebGL three.js】クォータニオンによる3次元回転と姿勢制御
現在、3次元グラフィックスの復習をしているよ。今回は3次元空間内の物体の姿勢を表すクォータニオンの復習と姿勢制御の練習を行うよ。その前に3次元空間上の任意の位置ベクトル($${\boldsymbol{r}}$$)を、任意の回転軸($${\boldsymbol{l}}$$)で回転($${\phi}$$)させたときの大切な公式の復習から始めるよ。回転後の位置ベクトルを $${\boldsymbol{r}'}$$ と表すと、次のような関係式が知られているね。
$$
\boldsymbol{r'} =\boldsymbol{r}\cos\phi +\boldsymbol{l} \times \boldsymbol{r} \sin\phi+\boldsymbol{r}\cdot\boldsymbol{l} (1-\cos\phi)\,\boldsymbol{l}
$$
この式はロドリーグの式と呼ばれるよ。この式を用いれば回転の計算は簡単にできるね。一方、コンピュータ・グラフィックスなどで3次元空間上の物体の姿勢を表す量として利用されるのが、クォータニオンだよ。クォータニオンは複素数の虚数「$${i}$$」に加えて「$${j}$$」「$${k}$$」を用意して、
$$
q = q_w+ iq_x+jq_y+kq_z
$$
で定義されるね。ただし、
$$
i^2 =-1 \ , \ j^2=-1 \ , \ k^2 = -1
$$
$$
ij =k\ , \ jk=i \ , \ ki = j
$$
$$
ji =-k\ , \ kj=-i \ , \ ik = -j
$$
という関係があるとするよ。また、複素数の複素共役と同様に共役四元数を
$$
q^\dagger = q_w -iq_x-jq_y-kq_z
$$
と定義すると、
$$
qq^\dagger = q_w^2 +q_x^2+q_y^2+q_z^2
$$
$$
q^\dagger q = q_w^2 +q_x^2+q_y^2+q_z^2
$$
が成り立つね。ここでクォータニオンの成分が
$$
q_w = \cos \frac{\phi}{2} \ , \ \sqrt{ q_x^2+q_y^2+q_z^2 } =\sin\frac{\phi}{2}
$$
を満たすように制限しておいて、単位ベクトル$${\boldsymbol{l}\ \left( |\boldsymbol{l}|=1\right)}$$を用意すると
$$
q= \cos \frac{\phi}{2} +\boldsymbol{l}\sin\frac{\phi}{2}
$$
$$
q^\dagger= \cos \frac{\phi}{2} -\boldsymbol{l}\sin\frac{\phi}{2}
$$
は単位クォータニオンになるね($${qq^\dagger= q^\dagger q =1}$$)。このように変な形でクォータニオンを定義したのには理由があるね。実は、この2つのクォータニオンで任意の位置ベクトル$${\boldsymbol{r}}$$を囲むと、計算結果は回転軸 $${\boldsymbol{l}}$$、回転角度$${\phi}$$の回転結果を与えるよ。
$$
\boldsymbol{r'} = q \boldsymbol{r}q^\dagger=\boldsymbol{r}\cos\phi +\boldsymbol{l} \times \boldsymbol{r} \sin\phi+\boldsymbol{r}\cdot\boldsymbol{l} (1-\cos\phi)\,\boldsymbol{l}
$$
先のロドリーグの式と一致するね。一般的な3次元グラフィックスでは、物体の姿勢は$${\hat{\boldsymbol{y}}=(0,1,0)}$$ からの任意軸回転で定義され、その時のクォータニオンを姿勢クォータニオンと呼ばれるよ。この値で任意の姿勢が表現できるわけだね。任意の姿勢の物体に対して、さらなる任意軸回転を考える場合、回転に対応するクォータニオンを前から積算することで、上向き($${\hat{\boldsymbol{y}}=(0,1,0) }$$)を基準とした姿勢クォータニオンを計算することができるよ。例えば、ある姿勢クォータニオンを
$$
q_2 = \beta+ \boldsymbol{b} =\beta + ib_x + jb_y+ kb_z
$$
と定義しておくと、物体を構成する任意の頂点ベクトルの回転前($${\boldsymbol{r}_0}$$)と回転後($${\boldsymbol{r}_2}$$)の関係は
$$
\boldsymbol{r}_1 =q_1 \boldsymbol{r}_0 q_1^\dagger
$$
次のよう表されるわけだけれども、さらにここから任意軸回転に対応するクォータニオンを
$$
q_1 =\alpha +\boldsymbol{a} =\alpha+ ia_x + ja_y+ ka_z
$$
を適用することを考えると
$$
\boldsymbol{r}_2 =q_2 \boldsymbol{r}_1 q_2^\dagger =q_2 q_1 \boldsymbol{r}_0 q_1^\dagger q_2^\dagger =q_2 q_1 \boldsymbol{r}_0 (q_2 q_1)^\dagger =q \boldsymbol{r}_0q^\dagger
$$
となるので、$${ q = q_2 q_1}$$ とクォータニオンの積、つまり、姿勢クォータニオンに任意軸回転クォータニオンを前から積算することで、上向きを基準とした新しい姿勢クォータニオンが得られるよ。あとに示すけれども、回転角度を微小とした任意軸回転クォータニオンを用意しておいて、レンダリングごとに積算していくと、回転アニメーションを作成することができるね。
では早速、姿勢クォータニオン・任意軸回転クォータニオンの復習のために下のような矢印型オブジェクトを生成するよ。WebGLのライブラリthree.jsには太さをもった矢印は用意されていないので、改めてArrow3Dクラスを定義しておくね。このクラスでは矢印の頭の幅と長さ、矢印の本体の幅をあらかじめ指定しておいて、本体の長さは矢印の始点と終点の位置を設定するメソッドで計算して実装することを考えるよ。
次の下のように始点を$${\boldsymbol{r}_{\rm bottom}=(0, 0, 0)}$$、終点を$${\boldsymbol{r}_{\rm top}=(10, 10, 10)}$$とした矢印の姿勢クォータニオンの与え方を考えるよ。
まず、矢印の始点から終点のベクトルを$${ \boldsymbol{L} = \boldsymbol{r}_{\rm top}- \boldsymbol{r}_{\rm bottom}}$$と定義しておいて、$${ \boldsymbol{L}}$$と基準となる上向きを表す単位ベクトル $${\hat{\boldsymbol{y}}}$$ で張られる平面に対する垂直方向の単位ベクトル($${\boldsymbol{n}}$$)を次のとおり計算するよ。
$$
\boldsymbol{n} = \frac{ \hat{\boldsymbol{y}} \times \boldsymbol{L} }{|\hat{\boldsymbol{y}} \times \boldsymbol{L}|}
$$
また、$${ \boldsymbol{L}}$$と$${ \hat{\boldsymbol{y}}}$$のなす角$${\theta}$$は、
$$
\cos\theta = \frac{\boldsymbol{L}\cdot\hat{\boldsymbol{y}}}{|\boldsymbol{L}|}
$$
で与えられるね。つまり、$${ \hat{\boldsymbol{y} }}$$から$${ \hat{\boldsymbol{L} }}$$への回転は、回転軸$${ \boldsymbol{n}}$$、回転角$${\theta}$$のクォータニオンがこの矢印の姿勢クォータニオンとなるね。three.jsでは回転軸と回転角からクォータニオンを簡単に生成することができるので、生成したクォータニオンをそのままTHREE.Object3Dクラスのquaternionプロパティに与えるだけだね。自作Arrow3Dクラスのプログラムを以下に示すよ。
class Arrow3D{
constructor( THREE, headWidth, headLength, bodyWidth , color ) {
this.THREE = THREE;
this.headWidth = headWidth;
this.headLength = headLength;
this.bodyWidth = bodyWidth;
this.color = color;
this.startPoint = 2 //1:中心、2:矢印の元
//円柱オブジェクトの生成
this.arrowHead = new this.THREE.Mesh(
new this.THREE.CylinderGeometry(0, headWidth, headLength, 50, 50),
new this.THREE.MeshPhongMaterial({ color: color, specular: 0xffffff, shininess: 250 })
//new this.THREE.MeshBasicMaterial({ color: color })
);
this.arrowHead.position.set( 0, - headLength/2, 0 );
//円柱オブジェクトの生成
this.arrowBody = new this.THREE.Mesh(
new this.THREE.CylinderGeometry(bodyWidth, bodyWidth, 1, 50, 50),
new this.THREE.MeshPhongMaterial({ color: color, specular: 0xffffff, shininess: 250 })
//new this.THREE.MeshBasicMaterial({ color: color })
);
this.arrowBody.position.set( 0, -headLength/2, 0 );
this.arrowBody.scale.set( 1, headLength, 1 );
this.CG = new this.THREE.Object3D( );
this.CG.arrowHead = this.arrowHead;
this.CG.arrowBody = this.arrowBody;
this.CG.add( this.arrowHead );
this.CG.add( this.arrowBody );
//初期姿勢
this.setArrowBottomToTop( {x:0, y:0, z:0}, {x:0, y:0, z:10} )
}
setArrowBottomToTop( bottom, top ){
//矢印の起点ベクトル
let L = new this.THREE.Vector3( ).subVectors( top, bottom );
//中心座標ベクトル
let R = new this.THREE.Vector3( ).addVectors( top, bottom ).multiplyScalar( 1/2 );
this.arrowBody.scale.set( 1, L.length() - this.headLength , 1 );
//3次元オブジェクトの基準位置を考慮して配置
if( this.startPoint == 1 ){
this.arrowBody.position.set( 0,- this.headLength/2, 0 );
this.arrowHead.position.set( 0, L.length()/2 - this.headLength/2, 0 );
this.CG.position.copy( R );
} else if( this.startPoint == 2 ){
this.arrowBody.position.set( 0, L.length()/2 - this.headLength/2, 0 );
this.arrowHead.position.set( 0, L.length() - this.headLength/2, 0 );
this.CG.position.copy( bottom );
}
//基準となるベクトル
let V1 = new this.THREE.Vector3(0, 1, 0);
//規格化した位置ベクトル
let V2 = L.clone().normalize();
//回転軸ベクトル
let V3 = new this.THREE.Vector3().crossVectors( V1, V2);
//回転角のcos値
let cosTheta = V1.x * V2.x + V1.y * V2.y + V1.z * V2.z;
//回転ありの場合
if( Math.abs(V3.length()) > 0.001 ){
//回転角
let theta = Math.acos( cosTheta );
//回転に対応するクオータニオンの生成
let q = new this.THREE.Quaternion().setFromAxisAngle(
V3.normalize(), theta
)
//姿勢クオータニオンに設定
this.CG.quaternion.copy( q );
}
}
setVisible( visible ){
this.arrowHead.visible = visible;
this.arrowBody.visible = visible;
}
}
これを
let arrow = new Arrow3D( THREE, 1, 2, 0.2, 0x00ffff );
arrow.setArrowBottomToTop( {x:0, y:0, z:0}, {x:10, y:10, z:10} )
こんな感じで生成してシーンに追加すると矢印を生成することができるね。
連続回転アニメーションの作成
下の図はz軸(画面上向き)を回転軸として、1ステップあたり$${\pi/100}$$ラジアン回転したときのアニメーションだよ。
先にも述べたとおり1ステップあたりの回転に対応するクォータニオンを生成しておいて、姿勢クォータニオンに前から積算することで回転させることができるね。
let q = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 0, 1), Math.PI/100
)
arrow.CG.quaternion.premultiply(q);
以上だよ。
プログラムソース全文(three.js)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>矢印</title>
<style>
*{padding:0px; margin:0px}
div#canvas-frame{
width: 1280px; /* 横幅 */
height: 720px; /* 縦幅 */
overflow:hidden;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.168.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.168.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { TrackballControls } from 'three/addons/controls/TrackballControls.js';
////////////////////////////////////////////////////////////////////
// windowイベントの定義
////////////////////////////////////////////////////////////////////
window.addEventListener("load", function () {
threeStart(); //Three.jsのスタート関数の実行
});
////////////////////////////////////////////////////////////////////
// Three.jsスタート関数の定義
////////////////////////////////////////////////////////////////////
function threeStart() {
initThree(); //Three.js初期化関数の実行
initLight(); //光源初期化関数の実行
initObject(); //オブジェクト初期化関数の実行
initCamera(); //カメラ初期化関数の実行
loop(); //無限ループ関数の実行
}
////////////////////////////////////////////////////////////////////
// Three.js初期化関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let renderer, //レンダラーオブジェクト
scene, //シーンオブジェクト
canvasFrame; //キャンバスフレームのDOM要素
function initThree() {
//キャンバスフレームDOM要素の取得
canvasFrame = document.getElementById('canvas-frame');
//レンダラーオブジェクトの生成
renderer = new THREE.WebGLRenderer({ antialias: true });
if (!renderer) alert('Three.js の初期化に失敗しました');
//レンダラーのサイズの設定
renderer.setSize(canvasFrame.clientWidth, canvasFrame.clientHeight);
//キャンバスフレームDOM要素にcanvas要素を追加
canvasFrame.appendChild(renderer.domElement);
//レンダラークリアーカラーの設定
renderer.setClearColor(0xEEEEEE, 1.0);
//シーンオブジェクトの生成
scene = new THREE.Scene();
}
////////////////////////////////////////////////////////////////////
// カメラ初期化関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let camera; //カメラオブジェクト
let trackball;
function initCamera() {
//カメラオブジェクトの生成
camera = new THREE.PerspectiveCamera(45, canvasFrame.clientWidth / canvasFrame.clientHeight, 1, 10000);
//カメラの位置の設定
camera.position.set(0, 50, 20);
//カメラの上ベクトルの設定
camera.up.set(0, 0, 1);
//カメラの中心位置ベクトルの設定
camera.lookAt({ x: 0, y: 0, z: 0 }); //トラックボール利用時は自動的に無効
//トラックボールオブジェクトの宣言
trackball = new TrackballControls(camera, canvasFrame);
//トラックボール動作範囲のサイズとオフセットの設定
trackball.screen.width = canvasFrame.clientWidth; //横幅
trackball.screen.height = canvasFrame.clientHeight; //縦幅
trackball.screen.offsetLeft = canvasFrame.getBoundingClientRect().left; //左オフセット
trackball.screen.offsetTop = canvasFrame.getBoundingClientRect().top; //右オフセット
//トラックボールの回転無効化と回転速度の設定
trackball.noRotate = false;
trackball.rotateSpeed = 2.0;
//トラックボールの拡大無効化と拡大速度の設定
trackball.noZoom = false;
trackball.zoomSpeed = 4.0;
//トラックボールのカメラ中心移動の無効化と中心速度の設定
trackball.noPan = false;
trackball.panSpeed = 1.0;
trackball.target = new THREE.Vector3(0, 0, 0);
//トラックボールのスタティックムーブの有効化
trackball.staticMoving = true;
//トラックボールのダイナミックムーブ時の減衰定数
trackball.dynamicDampingFactor = 0.3;
}
////////////////////////////////////////////////////////////////////
// 光源初期化関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let directionalLight, //平行光源オブジェクト
ambientLight; //環境光オブジェクト
function initLight() {
//平行光源オブジェクトの生成
directionalLight = new THREE.DirectionalLight(0xffffff, 2.0, 0);
//平行光源オブジェクトの位置の設定
directionalLight.position.set(0, 50, 50);
//環境光オブジェクトの生成
ambientLight = new THREE.AmbientLight(0x555555);
//環境光オブジェクトのシーンへの追加
scene.add(ambientLight);
//平行光源オブジェクトのシーンへの追加
scene.add(directionalLight);
}
////////////////////////////////////////////////////////////////////
// オブジェクト初期化関数の定義
////////////////////////////////////////////////////////////////////
let arrow;
function initObject() {
//スカイドームの生成
let skydome = createSkydome();
//床オブジェクトをシーンへ追加
scene.add( skydome );
//床オブジェクトの生成
let floor = createFloor();
//床オブジェクトをシーンへ追加
scene.add( floor );
arrow = new Arrow3D( THREE, 1, 2, 0.2, 0x00ffff );
arrow.setArrowBottomToTop( {x:0, y:0, z:0}, {x:10, y:10, z:10} )
scene.add( arrow.CG );
}
class Arrow3D{
constructor( THREE, headWidth, headLength, bodyWidth , color ) {
this.THREE = THREE;
this.headWidth = headWidth;
this.headLength = headLength;
this.bodyWidth = bodyWidth;
this.color = color;
this.startPoint = 2 //1:中心、2:矢印の元
//円柱オブジェクトの生成
this.arrowHead = new this.THREE.Mesh(
new this.THREE.CylinderGeometry(0, headWidth, headLength, 50, 50),
new this.THREE.MeshPhongMaterial({ color: color, specular: 0xffffff, shininess: 250 })
//new this.THREE.MeshBasicMaterial({ color: color })
);
this.arrowHead.position.set( 0, - headLength/2, 0 );
//円柱オブジェクトの生成
this.arrowBody = new this.THREE.Mesh(
new this.THREE.CylinderGeometry(bodyWidth, bodyWidth, 1, 50, 50),
new this.THREE.MeshPhongMaterial({ color: color, specular: 0xffffff, shininess: 250 })
//new this.THREE.MeshBasicMaterial({ color: color })
);
this.arrowBody.position.set( 0, -headLength/2, 0 );
this.arrowBody.scale.set( 1, headLength, 1 );
this.CG = new this.THREE.Object3D( );
this.CG.arrowHead = this.arrowHead;
this.CG.arrowBody = this.arrowBody;
this.CG.add( this.arrowHead );
this.CG.add( this.arrowBody );
//初期姿勢
this.setArrowBottomToTop( {x:0, y:0, z:0}, {x:0, y:0, z:10} )
}
setArrowBottomToTop( bottom, top ){
//矢印の起点ベクトル
let L = new this.THREE.Vector3( ).subVectors( top, bottom );
//中心座標ベクトル
let R = new this.THREE.Vector3( ).addVectors( top, bottom ).multiplyScalar( 1/2 );
this.arrowBody.scale.set( 1, L.length() - this.headLength , 1 );
//3次元オブジェクトの基準位置を考慮して配置
if( this.startPoint == 1 ){
this.arrowBody.position.set( 0,- this.headLength/2, 0 );
this.arrowHead.position.set( 0, L.length()/2 - this.headLength/2, 0 );
this.CG.position.copy( R );
} else if( this.startPoint == 2 ){
this.arrowBody.position.set( 0, L.length()/2 - this.headLength/2, 0 );
this.arrowHead.position.set( 0, L.length() - this.headLength/2, 0 );
this.CG.position.copy( bottom );
}
//基準となるベクトル
let V1 = new this.THREE.Vector3(0, 1, 0);
//規格化した位置ベクトル
let V2 = L.clone().normalize();
//回転軸ベクトル
let V3 = new this.THREE.Vector3().crossVectors( V1, V2);
//回転角のcos値
let cosTheta = V1.x * V2.x + V1.y * V2.y + V1.z * V2.z;
//回転ありの場合
if( Math.abs(V3.length()) > 0.001 ){
//回転角
let theta = Math.acos( cosTheta );
//回転に対応するクオータニオンの生成
let q = new this.THREE.Quaternion().setFromAxisAngle(
V3.normalize(), theta
)
//姿勢クオータニオンに設定
this.CG.quaternion.copy( q );
}
}
setVisible( visible ){
this.arrowHead.visible = visible;
this.arrowBody.visible = visible;
}
}
////////////////////////////////////////////////////////////////////
// 無限ループ関数の定義
////////////////////////////////////////////////////////////////////
//グローバル変数の宣言
let step = 0; //ステップ数
function loop() {
//トラックボールによるカメラオブジェクトのプロパティの更新
trackball.update();
//ステップ数のインクリメント
step++;
let q = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 0, 1), Math.PI/100
)
arrow.CG.quaternion.premultiply(q);
// if( step%100 == 0) console.log(step);
//レンダリング
renderer.render(scene, camera);
//画像生成
makePicture();
//「loop()」関数の呼び出し
requestAnimationFrame(loop);
}
//床オブジェクト生成関数
function createFloor(){
let textureLoader = new THREE.TextureLoader();
let texture = textureLoader.load( generateFloorTextureDataURL( [0xFFFFFF, 0x555555] ) )
//テクスチャラッピングの指定
texture.wrapS = THREE.RepeatWrapping; //x軸方向
texture.wrapT = THREE.RepeatWrapping; //y軸方向
//リピートの指定
texture.repeat.set(10, 10);
//カラースペースの指定
texture.colorSpace = THREE.SRGBColorSpace;
texture.mapWrapT = "RepeatWrapping";
texture.mapWrapS = "RepeatWrapping";
//異方性フィルダリング指数の指定
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
//形状オブジェクトの宣言と生成
let geometry = new THREE.PlaneGeometry(50, 50); //平面オブジェクト
//材質オブジェクトの宣言と生成
let material = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: texture,
side: THREE.DoubleSide
});
//平面オブジェクトの生成
return new THREE.Mesh(geometry, material);;
}
//スカイドーム生成関数
function createSkydome(){
let vertexShader = "//バーテックスシェーダー\n" +
"//頂点シェーダーからフラグメントシェーダーへの転送する変数\n" +
"varying vec3 vWorldPosition;\n" +
"void main( ) {\n" +
" //ワールド座標系における頂点座標\n" +
" vec4 worldPosition = modelMatrix * vec4( position, 1.0 );\n" +
" vWorldPosition = worldPosition.xyz;\n" +
" gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n" +
"}\n";
let fragmentShader = "//フラグメントシェーダ―\n" +
"//カスタムuniform変数の取得\n" +
"uniform vec3 topColor; //ドーム頂点色\n" +
"uniform vec3 bottomColor; //ドーム底辺色\n" +
"uniform float exp; //減衰指数\n" +
"uniform float offset; //高さ基準点\n" +
"//バーテックスシェーダーから転送された変数\n" +
"varying vec3 vWorldPosition;\n" +
"void main( ) {\n" +
" //高さの取得\n" +
" float h = normalize( vWorldPosition + vec3(0, 0, offset) ).z;\n" +
" if( h < 0.0) h = 0.0;\n" +
" gl_FragColor = vec4( mix( bottomColor, topColor, pow(h, exp) ), 1.0 );\n" +
"}\n";
//形状オブジェクトの宣言と生成
let geometry = new THREE.SphereGeometry( 500, 100, 100);
let uniforms = {
topColor: { type: "c", value: new THREE.Color(0.0, 0.3, 1.0) },
bottomColor: { type: "c", value: new THREE.Color(1.0, 1.0, 1.0)},
exp:{ type: "f", value : 0.8},
offset:{ type: "f", value : 200}
};
//材質オブジェクトの宣言と生成
let material = new THREE.ShaderMaterial( {
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms : uniforms,
side: THREE.BackSide,
} );
return new THREE.Mesh( geometry, material);
}
//////////////////////////////////////////////
// テクスチャマッピング用 dataURL生成関数
//////////////////////////////////////////////
function generateFloorTextureDataURL( tileColors ){
//canvas要素の生成
let canvas = document.createElement('canvas');
//canvas要素のサイズ
canvas.width = 256; //横幅
canvas.height = 256; //縦幅
//コンテキストの取得
let context = canvas.getContext('2d');
let n = 2;
let colors = [];
colors[0] = "#" + tileColors[0].toString(16);
colors[1] = "#" + tileColors[1].toString(16);
colors[0].replace("0x" , "");
colors[1].replace("0x" , "");
for ( let i = 0; i < n; i++ ) {
for ( let j = 0; j < n; j++ ) {
context.beginPath();
context.rect( i*canvas.width/n, j*canvas.height/n, canvas.width/n, canvas.height/n );
context.closePath();
let m = Math.abs( i + j ) % colors.length;
//塗りの設定
context.fillStyle = colors[m]; //塗りつぶし色の指定
context.fill(); //塗りつぶしの実行
}
}
let dataUrl = canvas.toDataURL("image/png");
return dataUrl;
}
//////////////////////////////////////////////
// 画像作成用
//////////////////////////////////////////////
let makePictureFlag = false;
//画像作成用イベント
window.addEventListener('keydown', function (e) {
//キーボードイベント時のキー取得
let keyChar = String.fromCharCode( e.keyCode ).toLowerCase();
//キーボードの「s」が押された場合
if(keyChar == "s") {
makePictureFlag = true;
}
});
//画像作成関数
function makePicture(){
if( !makePictureFlag ) return;
//グラフィックスが描画されたcanvas要素
let canvas = renderer.domElement;
//a要素の生成
let a = document.createElement("a");
//canvas要素→DataURL形式
a.href = canvas.toDataURL("image/png");
//PNGファイル名の命名
a.download = "picture";
a.innerHTML = "ダウンロード";
//id="thumbnails"のdiv要素の子要素にa要素を追加
document.getElementsByTagName( "body" )[0].appendChild(a);
makePictureFlag = false;
}
</script>
</head>
<body>
<div id="canvas-frame"></div><!-- canvas要素を配置するdiv要素 -->
</body>
</html>
この記事が気に入ったらサポートをしてみませんか?