前回は変位勾配テンソルの成分に定数を与えました。テンソルの成分はもちろん位置によって変化する変数でもよいので、今回はz軸周りの回転量をz値によって変化させることで、ツイストを作ってみるね。もともとz軸周りの回転行列は
$$
R = \left( \begin{matrix} \cos\theta & \sin\theta & 0 \cr -\sin\theta & \cos\theta & 0 \cr 0 & 0 & 0 \end{matrix} \right)
$$
となるので、この$$\theta=\alpha z$$のように与えることでz軸周りのツイストを実現できるね。$${\alpha}$$がツイストの単位長さあたりの回転量(ラジアン)だね。下図が結果だよ。今回もthree.jsを用いて作ってみました。

ちなみにこのツイストに対応する変形勾配テンソルは先の回転行列をテーラー展開して2次以降を無視しておいて、対角成分から1を引いた値、
$$
D = \left( \begin{matrix} 0 & \alpha z & 0 \cr -\alpha z & 0 & 0 \cr 0 & 0 & 0 \end{matrix} \right)
$$
となるね。変位勾配テンソルの成分には連続体としての整合性を保証するための条件「歪の適合方程式」
$$
\frac{\partial ^2 E_{xx}}{\partial y \partial z} = \frac{\partial}{\partial x} \left(-\frac{\partial E_{yz}}{\partial x} + \frac{\partial E_{zx}}{\partial y} +\frac{\partial E_{xy}}{\partial z} \right)
$$
$$
\frac{\partial ^2 E_{yy}}{\partial z \partial x} = \frac{\partial}{\partial y} \left(-\frac{\partial E_{zx}}{\partial y} + \frac{\partial E_{xy}}{\partial z} +\frac{\partial E_{yz}}{\partial x} \right)
$$
$$
\frac{\partial ^2 E_{zz}}{\partial x \partial y} = \frac{\partial}{\partial z} \left(-\frac{\partial E_{xy}}{\partial z} + \frac{\partial E_{yz}}{\partial x} +\frac{\partial E_{zx}}{\partial y} \right)
$$
$$
2\,\frac{\partial ^2 E_{xy}}{\partial x \partial y} =\frac{\partial^2 E_{xx}}{\partial y^2} +\frac{\partial^2 E_{yy}}{\partial x^2}
$$
$$
2\,\frac{\partial ^2 E_{yz}}{\partial y \partial z} =\frac{\partial^2 E_{yy}}{\partial z^2} +\frac{\partial^2 E_{zz}}{\partial y^2}
$$
$$
2\,\frac{\partial ^2 E_{zx}}{\partial z \partial x} =\frac{\partial^2 E_{zz}}{\partial x^2} +\frac{\partial^2 E_{xx}}{\partial z^2}
$$
を満たす必要があるね。$${E_{ij}}$$は歪テンソルの成分なのだけれども、ツイストはそもそも歪成分を持たないので、どれだけツイストしても問題ないってことだね。
Javascriptプログラムソース
Javascriptプログラムソースを用意したよ。もし良かったら試してみてください。これからも応援よろしくお願いしまーす!
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>変位勾配テンソルによる変形(変数:ツイスト)</title><script src="https://natural-science.or.jp/files/physics/UV_Grid_Sm.js"></script><script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script><style>*{padding:0px; margin:0px}div#canvas-frame{width: 1200px; /* 横幅 */height: 900px; /* 縦幅 */overflow:hidden;}table.matrix input{width: 30px;}section#field{padding: 10px 20px;border: 1px solid gray;border-radius: 10px;margin: 10px 10px;width: 1140px;}</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';//初期変位勾配テンソルlet D = [[0, 0, 0],[0, 0, 0],[0, 0, 0]]let theta_max = 4.0 * Math.PI;let Lz = 2;function setD( r, theta, D ){let x = r[0], y = r[1], z = r[2];let _theta = theta * z / Lz;D[0][0] = Math.cos( _theta ) - 1.0;D[0][1] = Math.sin( _theta );D[0][2] = 0;D[1][0] = - Math.sin( _theta );D[1][1] = Math.cos( _theta ) - 1.0;D[1][2] = 0;D[2][0] = 0.0;D[2][1] = 0.0;D[2][2] = 0.0;}//変位勾配テンソルによる変位計算function Deformation( D, r ){let x = D[ 0 ][ 0 ] * r[ 0 ] + D[ 0 ][ 1 ] * r[ 1 ] + D[ 0 ][ 2 ] * r[ 2 ];let y = D[ 1 ][ 0 ] * r[ 0 ] + D[ 1 ][ 1 ] * r[ 1 ] + D[ 1 ][ 2 ] * r[ 2 ];let z = D[ 2 ][ 0 ] * r[ 0 ] + D[ 2 ][ 1 ] * r[ 1 ] + D[ 2 ][ 2 ] * r[ 2 ];return [ x, y, z ];}//格子点座標の初期値let initialPositionArray = [ ]////////////////////////////////////////////////////////////////////// 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(2.2, 2.2, 1.2);//カメラの上ベクトルの設定camera.up.set(0, 0, 1);//カメラの中心位置ベクトルの設定camera.lookAt(new THREE.Vector3(0,0,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(50, 50, 50);//平行光源オブジェクトの影の生成元directionalLight.castShadow = false;//平行光源オブジェクトのシーンへの追加scene.add(directionalLight);//環境光オブジェクトの生成ambientLight = new THREE.AmbientLight(0x666666);//環境光オブジェクトのシーンへの追加scene.add(ambientLight);}////////////////////////////////////////////////////////////////////// オブジェクト初期化関数の定義////////////////////////////////////////////////////////////////////let box;function initObject() {//スカイドームの生成let skydome = createSkydome();//床オブジェクトをシーンへ追加scene.add( skydome );//床オブジェクトの生成let floor = createFloor();//床オブジェクトをシーンへ追加//scene.add( floor );//img要素の生成let img = new Image(); img.src = UV_Grid_Sm; //DataURLlet texture = new THREE.Texture( img );texture.needsUpdate = true;//カラースペースの指定texture.colorSpace = THREE.SRGBColorSpace;let L = 1;//形状オブジェクトの宣言と生成let geometry = new THREE.BoxGeometry(L, //width:x軸方向の幅(デフォルト:1.0)L, //height:y軸方向の幅(デフォルト:1.0)Lz, //depth:z軸方向の幅(デフォルト:1.0)100, //widthSegments:x軸方向の分割数(デフォルト:1)100, //heightSegments:y軸方向の分割数(デフォルト:1)100 //depthSegments:z軸方向の分割数(デフォルト:1));//材質オブジェクトの宣言と生成let material = new THREE.MeshPhongMaterial({ map: texture, color: 0xffffff, specular: 0xffffff, shininess: 200 })//直方体オブジェクトの生成box = new THREE.Mesh(geometry, material);//直方体オブジェクトのシーンへの追加scene.add(box);//直方体オブジェクトの位置座標を設定box.position.set(-L/2, -L/2, -L*1.2);for(let i = 0; i < box.geometry.attributes.position.array.length/3; i++){box.geometry.attributes.position.array[ 3 * i + 0 ] += 0; box.geometry.attributes.position.array[ 3 * i + 1 ] += 0; box.geometry.attributes.position.array[ 3 * i + 2 ] += Lz/2;}for(let i = 0; i < box.geometry.attributes.position.array.length; i++){initialPositionArray.push( box.geometry.attributes.position.array[i] );}let arrowY = new Arrow3D( THREE, 0.1, 0.2, 0.02, 0x00ff00 );arrowY.setArrowBottomToTop( {x:-L/2, y:-L/2, z:-L*1.2}, {x:-L/2, y:1.2*L, z:-L*1.2} )scene.add( arrowY.CG );let arrowX = new Arrow3D( THREE, 0.1, 0.2, 0.02, 0xff0000 );arrowX.setArrowBottomToTop( {x:-L/2, y:-L/2, z:-L*1.2}, {x:1.2*L, y:-L/2, z:-L*1.2} )scene.add( arrowX.CG );let arrowZ = new Arrow3D( THREE, 0.1, 0.2, 0.02, 0x0000ff );arrowZ.setArrowBottomToTop( {x:-L/2, y:-L/2, z:-L*1.2}, {x:-L/2, y:-L/2, z:1.2*L} )scene.add( arrowZ.CG );}////////////////////////////////////////////////////////////////////// 無限ループ関数の定義//////////////////////////////////////////////////////////////////////グローバル変数の宣言let step = 0; //ステップ数function loop() {//トラックボールによるカメラオブジェクトのプロパティの更新trackball.update();let theta = theta_max * (step%2000) / 2000;for(let i = 0; i < box.geometry.attributes.position.array.length/3; i++){let x = initialPositionArray[ 3 * i + 0 ]; let y = initialPositionArray[ 3 * i + 1 ]; let z = initialPositionArray[ 3 * i + 2 ];let dx = [x, y, z];setD( dx, theta, D )let dr = Deformation( D, dx );box.geometry.attributes.position.array[ 3 * i + 0 ] = dx[ 0 ] + dr[ 0 ];box.geometry.attributes.position.array[ 3 * i + 1 ] = dx[ 1 ] + dr[ 1 ];box.geometry.attributes.position.array[ 3 * i + 2 ] = dx[ 2 ] + dr[ 2 ];}box.geometry.attributes.position.needsUpdate = true;box.geometry.computeVertexNormals();step ++;//レンダリングrenderer.render(scene, camera);//「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(5, 5); //平面オブジェクト//材質オブジェクトの宣言と生成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" +"uniformfloat exp; //減衰指数\n" +"uniformfloat 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 : 400}};//材質オブジェクトの宣言と生成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;}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:10, z:0} )}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;}}</script></head><body><section id="field"><div style="font-size: 20pt; padding: 10px 10px 0px 10px; float: left;">\( \boldsymbol{r} = \left( \matrix{ x(\cos\theta-1) + y\sin\theta \cr -x\sin\theta + y(\cos\theta - 1) \cr 0 } \right)\ \to \ D = \frac{\partial \boldsymbol{r}}{\partial \boldsymbol{x}} = \left( \matrix{ \cos\theta - 1 & \sin\theta & 0 \cr -\sin\theta & \cos\theta -1 & 0 \cr 0 & 0 & 0 } \right) \)</div><br style="clear:both;"></section><div id="canvas-frame"></div><!-- canvas要素を配置するdiv要素 --></body></html>