THREE.LatheBufferGeometryでグニュグニュ動く回転体を作ろう

Three.jsで用意されている[LatheBufferGeometry](https://threejs.org/docs/index.html#api/en/geometries/LatheBufferGeometry)。



こちらは断面形状に相当する**THREE.Vector2(x,y)配列を渡すだけで、回転体のメッシュを作ってくれる**
Geometryメソッド。
インタラクティブに変更可能な断面形状を作成すれば、グニュグニュっと動く立体が、サクッと作れてしまいます。


<p class="codepen" data-height="342" data-theme-id="dark" data-default-tab="result" data-user="koooood" data-slug-hash="ExamYdx" style="height: 342px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="THREE.LatheBufferGeometry">
  <span>See the Pen <a href="https://codepen.io/koooood/pen/ExamYdx">
  THREE.LatheBufferGeometry</a> by k (<a href="https://codepen.io/koooood">@koooood</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>



さらに[環境マテリアル](https://threejs.org/examples/?q=webgl#webgl_materials_envmaps)を設定すれば、不思議感倍増。うーむ凄いぞwebGL&three.js!!
 **動かしたいのはmeshの塊ではなくて、geometryの各頂点！**と思われている方、
ちょっとお試しでいかがでしょうか。
![capcher.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481748/c995260d-1ded-a227-aca2-c5b58b018a2e.gif)
[>>sample link](http://brain2.jp/koooood/threejs/MathTestBufferL3_v11e.html)

ちなみにthree.js各頂点を動かすのに**[morph](https://threejs.org/examples/?q=morph#webgl_morphtargets_sphere)**がありますが、想定内の軌跡をたどるパラパラ漫画に近く、今回の**ジェネレイティブに形を変化させる**目的には沿いません。

公式docページの**[How-to-update-things](https://threejs.org/docs/index.html#manual/en/introduction/How-to-update-things)のBufferGeometry**箇所によると、今回留意すべきは下記２点、
1. １次配列のbuffer(attribute）の領域を用意したら、領域（配列の長さ）を変えないように
2. アップデートは(geometry attribute).needsUpdate = true; の指示が、初回レンダ後に必要


一度確保した領域内であれば、geometry.attributeの値をアップデートする事ができるので、今回はLatheBufferGeometryが書き出してくれる、attribute('position')('normal')('uv')の３つ内、('position')('normal')にアクセスして、変更データを上書きする事にします。
面倒な全体の計算は任せてしまい、担当分野の**断面形状のみ！頑張ります。**

大まかな流れは、
**１）断面形状に対応するTHREE.Vector2(x,y)配列を作る**
　　**・係数値をdat.GUIで変更可能にしておく**
２）THREE.LatheBufferGeometryを作成
３）meshを作成しシーンにaddする
**４）レンダリング**
　　**・（2）geometryのattributeを再計算アップデート**

ポイントをかいつまんで解説します。詳細はこの[CodePen](https://codepen.io/koooood/pen/ExamYdx)をご確認ください。(環境マッピングは画像をアップできなかったので、MeshNormalMaterialとなっています）


###（１）断面形状を作る
dat.GUIで係数値を変化させると、(x,y)の値が変わる点群を作成します。
係数値を後に変化しても配列の数が変わらない範囲で、下記参考例にとらわれず自由にどうぞ。
dat.GUIは["1.Basic-Usage"](https://workshop.chromeexperiments.com/examples/gui/#1--Basic-Usage),["9.Updating-the-Display-Automatically"](https://workshop.chromeexperiments.com/examples/gui/#9--Updating-the-Display-Automatically)のサンプルを参考にしました。controls内オブジェクトにデフォルトの係数値を入れ、geometryの計算式とdat.GUI両方に渡って表記するイメージです。

```javascript

'係数デフォルト値をオブジェクトにセット'
var controls = {
    baseR : 6.8, turnNumA : 1, turnNumB : 40,
    ...
}

var gui = new dat.GUI();
    ...
    gui.add(controls, 'baseR',0,30);
    var f1 = gui.addFolder('Mix wave num');'フォルダー化'
    f1.add(controls, 'turnNumA',0.5,30);'GUIから操作'
    f1.add(controls, 'turnNumB',0.5,15).listen();'自動読み込み'
    ...
    f1.open();'フォルダーオープン'
```
---
**3つのサインカーブをブレンドした"x"値**
参考例として少々長くなりますがお付き合いを、今回は複雑感を出す為に、３つの異なるサインカーブ（概念的なカーブ実際の線ではない）curveA,B,Cを一つに合わせた、連続性を持った可変する数値配列"x"を作成しました。
curveA,B,Cは「波の大きさ」と「波の数」２つの係数値で違いを作ります。ちょっと複雑に見えますが、やっている事はシンプルです。
**x = (波の大きさ）×　sin((全体波の数 = ~pi)÷分割数）＋　ベース半径**
配列の数を変えずに可変する数値を割り出すのに便利な、THREE.Math.mapLinear()を使用しました。
![sincurveXY.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481748/d7a9a797-7d08-bef8-2778-d17b3744678a.gif)
   *波の数 = turnNum | 分割数　= divideNum | 波の総数/分割数 = θ　| *サインカーブの波の大きさ = ks | *回転体の基本半径 =　baseR | *３つのカーブの比重 = p

```javascript

'3curve mix for x'
    function funCurveMaker(num) {
        var baseMake = new Array(num+1).fill(1)
        .map((e,k) => ( e * k ))
        '分割数は高さの計算法と整合性をとるためnum+1としています。'
        'baseMakeは１から始まるint配列'
       
        var curveA = baseMake                
        .map((e) => THREE.Math.mapLinear(e, 0, num, 0, Math.PI * controls.turnNumA))
        .map((e) => controls.ksA * Math.sin(e) + controls.baseR) 
        'controls.xxxはdat.GUIで指示した係数値'
        var curveB ...
        var curveC ...
        
        var funCurve = new Array(num+1).fill(0)
        .map((e, k ) => curveA[k] * controls.pa + curveB[k] * controls.pb + curveC[k] * controls.pc)
        return funCurve;
    }

```

yは単純に設定した高さの分割です。各配列をTHREE.vectore2(x,y)の配列にセットするように、今回の例ではmakeVec2array()と関数にしています。（補足---デフォルトでも動きを出すように、時間軸でcurveBと、全体のメッシュを微妙に動かしています。）


###（２）THREE.LatheBufferGeometryを作成
後は[THREE.LatheBufferGeometry](https://threejs.org/docs/index.html#api/en/geometries/LatheBufferGeometry)の引数にTHREE.Vector2配列を入れれば、回転体が成形されます。第３引数以降はデフォルト値でＯＫ。

```javascript


    function makeGeo(){
        'LatheBufferGeometry(vec2 array, 回転方向セグメント)'
        myGeo = new THREE.LatheBufferGeometry(makeVec2array(),40);  
        return myGeo;
    };

```
###（３）名前付きmeshを作成
```javascript

'関数にする必要もないけど分かりやすいので'
    function createMesh(){ 
        var myMesh = new THREE.Mesh(makeGeo(),envMaterial);
        myMesh.name = "myMesh";
        '後にattributeを呼出すために、名前を付けておくと便利'
        myMesh.position.y = -(controls.height/2);
        myMesh.position.x = -30;
        scene.add(myMesh);
    }
```
肝はmyMesh.nameです。名前があると上位階層から呼出すのが楽。そしてお好きなマテリアルを割り当ててください。

###（４）フレーム毎にGeometryのattribute値を書き換え
レンダリング・フレーム毎に新しい係数値を反映したgeometryを表示させるために、再計算した値をgeometryのattributeに上書きします。
シーンに投入後のgeometryのattributeにアクセスするには、先ほどmeshに割当てた名前を使用して

```javascript

var mesh = scene.getObjectByName("myMesh");
mesh.geometry.attributes.position;
```
とします。sceneに入れたオブジェクトのそのまた下の階層の...ってやっていくと、その時の呼び出し階層で複雑になるので、getObjectByNameが一番確実かと。

そして、係数値変化後のgeometryを再計算しダミー変数に代入-->attribute 値を取り出し-->現在のgeometryのattribute.arrayに上書きします。.needsUpdate = trueとし、レンダリングに反映します。

```javascript

    function makeGeoForLoop(){
        'tempGeoで現在プロパティ値を再計算仮Geometryを作成'
        tempGeo = new THREE.LatheBufferGeometry(makeVec2array(),40);
        
        'それぞれ旧 attribute.array に上書き。コマンドはattaribute、上書き内容はarray.'
        '混乱しないように'
        //　(a)　<- (b) 
        mesh.geometry.attributes.position
                .copyArray(tempGeo.attributes.position.array)
                .needsUpdate = true;
    
        mesh.geometry.attributes.normal
                .copyArray(tempGeo.attributes.normal.array)
                .needsUpdate = true;

        // normalをコピーする代わりにこちらでもOK
        'mesh.geometry.computeBoundingSphere();'
    };

```
ポイントは以上です。シーンに上げたgeometryにアクセスするにはscene.xxx.から始めないといけないのですが、geometryの状態のままの場合はgeometory.xxxからの呼び出しで可能です。また、呼び出し表記はいくつかあるのですが、書いたようにmeshに名前を付けて呼出す方法が確実で簡単です。（単純なのですが、ウキーッとなるポイント）

さらに映り込みを求める方は、公式サイトの[環境マッピング](https://threejs.org/examples/?q=webgl#webgl_materials_envmaps)の例をご参考に。
threejsサンプル画面右下の＜＞で[git](https://github.com/mrdoob/three.js/blob/master/examples/webgl_materials_envmaps.html)上のコードが確認できます。
＋＋＋＋
tempGeoでインスタンス化を毎フレームして大丈夫か？！と生成した後にごそっと削除する事をしたのですが、こちらの方が重くなるようでした。ブラウザー偉い！iPhone11safariでもさっくり動いてます。

さらに、真偽の程は確かではないのですが過去のメモが残されていました。うーむ現在、意味不明。
>
environment texture にしたらPC負荷が軽い。
どうやらshaderMaterial は自動的にMesh(geometry,shaderMaterial)でgeometryのattributeをfragmentShaderに渡してくれる。geometryのattribute('position')/('normal')を書き換えるだけで　vertex shader を通し　fragment shader上で位置情報を元に描画される様子
>

＋＋＋＋
threejs2019のアドベントカレンダーに手を挙げたのですが、間に合わず。アンドウ様、関係者の皆様大変申し訳ございませんでした。なにとぞお許しを。
＋＋＋＋
独学サンデープログラマーなので、なにかご指摘の箇所あればお教え下さい。この題材できた時にtweetしたのですが、コードの書き方に今ひとつ自信が持てず、お蔵行きとなっていたものです。three.js-v92でした。現在cdnはv110が最新で、だいぶすっきり書ける様になった感があります。