LoginSignup
8
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-29

Three.jsで用意されているLatheBufferGeometry

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

See the Pen THREE.LatheBufferGeometry by k (@koooood) on CodePen.

さらに環境マテリアルを設定すれば、不思議感倍増。うーむ凄いぞwebGL&three.js!!
動かしたいのはmeshの塊ではなくて、geometryの各頂点!と思われている方、
ちょっとお試しでいかがでしょうか。
capcher.gif
>>sample link

ちなみにthree.js各頂点を動かすのにmorphがありますが、想定内の軌跡をたどるパラパラ漫画に近く、今回のジェネレイティブに形を変化させる目的には沿いません。

公式docページのHow-to-update-thingsのBufferGeometry箇所によると、今回留意すべきは下記2点、
1. 1次配列のbuffer(attribute)の領域を用意したら、領域(配列の長さ)を変えないように
2. アップデートは(geometry attribute).needsUpdate = true; の指示が、初回レンダ後に必要

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

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

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

(1)断面形状を作る

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


'係数デフォルト値をオブジェクトにセット'
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"値
参考例として少々長くなりますがお付き合いを、今回は複雑感を出す為に、3つの異なるサインカーブ(概念的なカーブ実際の線ではない)curveA,B,Cを一つに合わせた、連続性を持った可変する数値配列"x"を作成しました。
curveA,B,Cは「波の大きさ」と「波の数」2つの係数値で違いを作ります。ちょっと複雑に見えますが、やっている事はシンプルです。
x = (波の大きさ)× sin((全体波の数 = ~pi)÷分割数)+ ベース半径
配列の数を変えずに可変する数値を割り出すのに便利な、THREE.Math.mapLinear()を使用しました。
sincurveXY.gif
*波の数 = turnNum | 分割数 = divideNum | 波の総数/分割数 = θ | *サインカーブの波の大きさ = ks | *回転体の基本半径 = baseR | *3つのカーブの比重 = p


'3curve mix for x'
    function funCurveMaker(num) {
        var baseMake = new Array(num+1).fill(1)
        .map((e,k) => ( e * k ))
        '分割数は高さの計算法と整合性をとるためnum+1としています。'
        'baseMakeは1から始まる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と、全体のメッシュを微妙に動かしています。)

(2)THREE.LatheBufferGeometryを作成

後はTHREE.LatheBufferGeometryの引数にTHREE.Vector2配列を入れれば、回転体が成形されます。第3引数以降はデフォルト値でOK。



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

(3)名前付きmeshを作成


'関数にする必要もないけど分かりやすいので'
    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です。名前があると上位階層から呼出すのが楽。そしてお好きなマテリアルを割り当ててください。

(4)フレーム毎にGeometryのattribute値を書き換え

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


var mesh = scene.getObjectByName("myMesh");
mesh.geometry.attributes.position;

とします。sceneに入れたオブジェクトのそのまた下の階層の...ってやっていくと、その時の呼び出し階層で複雑になるので、getObjectByNameが一番確実かと。

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


    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に名前を付けて呼出す方法が確実で簡単です。(単純なのですが、ウキーッとなるポイント)

さらに映り込みを求める方は、公式サイトの環境マッピングの例をご参考に。
threejsサンプル画面右下の<>でgit上のコードが確認できます。
++++
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が最新で、だいぶすっきり書ける様になった感があります。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5