Three.jsで用意されているLatheBufferGeometry。
こちらは断面形状に相当するTHREE.Vector2(x,y)配列を渡すだけで、回転体のメッシュを作ってくれる
Geometryメソッド。
インタラクティブに変更可能な断面形状を作成すれば、グニュグニュっと動く立体が、サクッと作れてしまいます。
See the Pen THREE.LatheBufferGeometry by k (@koooood) on CodePen.
さらに環境マテリアルを設定すれば、不思議感倍増。うーむ凄いぞwebGL&three.js!!
**動かしたいのはmeshの塊ではなくて、geometryの各頂点!**と思われている方、
ちょっとお試しでいかがでしょうか。
>>sample link
ちなみにthree.js各頂点を動かすのに**morphがありますが、想定内の軌跡をたどるパラパラ漫画に近く、今回のジェネレイティブに形を変化させる**目的には沿いません。
公式docページの**How-to-update-thingsのBufferGeometry**箇所によると、今回留意すべきは下記2点、
- 1次配列のbuffer(attribute)の領域を用意したら、領域(配列の長さ)を変えないように
- アップデートは(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()を使用しました。
*波の数 = 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が最新で、だいぶすっきり書ける様になった感があります。