three.jsで3Dキャラを作成:腕を作る(3)

前回はひじを作成したので、今回はひじ〜腕の先までを作成して腕全体を完成させる。

ひじと下側の腕を別メッシュではなくひとつのメッシュとして作ることにした。

パラメータの設定

まずメッシュ作成に必要なパラメータを設定する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let lowerArmGeo;
const lowerArmObj = new Limbs();
const lowerArmThicks = new Array( limbSeg );
const lowerArmWidths = new Array( limbSeg );
for( let i=0; i<( limbSeg+1 ); i++ ){
const t = i / limbSeg;
lowerArmWidths[i] = upperArmThick - pow( t, 3 ) * upperArmThick;
lowerArmThicks[i] = upperArmThick - pow( t, 5 ) * upperArmThick;
}
lowerArmObj.thick = lowerArmThicks;
lowerArmObj.width = lowerArmWidths;
let lowerArmGeo; const lowerArmObj = new Limbs(); const lowerArmThicks = new Array( limbSeg ); const lowerArmWidths = new Array( limbSeg ); for( let i=0; i<( limbSeg+1 ); i++ ){ const t = i / limbSeg; lowerArmWidths[i] = upperArmThick - pow( t, 3 ) * upperArmThick; lowerArmThicks[i] = upperArmThick - pow( t, 5 ) * upperArmThick; } lowerArmObj.thick = lowerArmThicks; lowerArmObj.width = lowerArmWidths;
let lowerArmGeo;
const lowerArmObj = new Limbs();
const lowerArmThicks = new Array( limbSeg );
const lowerArmWidths = new Array( limbSeg );
for( let i=0; i<( limbSeg+1 ); i++ ){
    const t = i / limbSeg;
    lowerArmWidths[i] = upperArmThick - pow( t, 3 ) * upperArmThick;
    lowerArmThicks[i] = upperArmThick - pow( t, 5 ) * upperArmThick;
}
lowerArmObj.thick = lowerArmThicks;
lowerArmObj.width = lowerArmWidths;

太さは、段々細くしていって最後に0にすることで先端が閉じたチューブの形にする。

腕の縦幅?の方をより細くすることで手を平べったい感じにしている。

ジオメトリ+メッシュの作成

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
const jointArmPt = makeJointPt( upperArmObj, -1 );
const lowerArmPt = makePipePt( lowerArmObj );
const lowerArmPts = jointArmPt.concat( lowerArmPt );
jointArmObj.seg *= 2;
lowerArmGeo = makeGeometry( jointArmObj, lowerArmPts );
const lowerArmMesh = new THREE.Mesh( lowerArmGeo, armMat );
const jointArmPt = makeJointPt( upperArmObj, -1 ); const lowerArmPt = makePipePt( lowerArmObj ); const lowerArmPts = jointArmPt.concat( lowerArmPt ); jointArmObj.seg *= 2; lowerArmGeo = makeGeometry( jointArmObj, lowerArmPts ); const lowerArmMesh = new THREE.Mesh( lowerArmGeo, armMat );
const jointArmPt = makeJointPt( upperArmObj, -1 );
const lowerArmPt = makePipePt( lowerArmObj );
const lowerArmPts = jointArmPt.concat( lowerArmPt );
jointArmObj.seg *= 2;
lowerArmGeo = makeGeometry( jointArmObj, lowerArmPts );
const lowerArmMesh = new THREE.Mesh( lowerArmGeo, armMat );

まずjoint(ひじ)部分の座標を前回と同じく作成(引数の-1はfacelessなメッシュにならないようにするための適当な角度の値)

次にlowerArmの座標を作成し、ひじの座標とconcatで合体させる。

合体させた座標でジオメトリーを作成。この時、セグメントの数が合体により2倍になっているので、jointArmObjのセグメント数を2倍にしてジオメトリー作成関数に渡している。

グルーピング+シーンに追加

今後指のメッシュもくっつけることなどを考えて、かたまりとして扱いやすくするためにグルーピングする。

armG ⇨ 腕全体

lowerArmG ⇨ jointから下(今後指もここに追加)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//グループを作る
const armG = new THREE.Group();
const lowerArmG = new THREE.Group();
//グルーピングする
lowerArmG.add( lowerArmMesh );
armG.add( upperArmMesh );
armG.add( lowerArmG );
//グループをシーンに追加
scene.add( armG );
//グループを作る const armG = new THREE.Group(); const lowerArmG = new THREE.Group(); //グルーピングする lowerArmG.add( lowerArmMesh ); armG.add( upperArmMesh ); armG.add( lowerArmG ); //グループをシーンに追加 scene.add( armG );
//グループを作る
const armG = new THREE.Group();
const lowerArmG = new THREE.Group();

//グルーピングする
lowerArmG.add( lowerArmMesh );
armG.add( upperArmMesh );
armG.add( lowerArmG );

//グループをシーンに追加
scene.add( armG );

曲げてみる

任意の角度でひじを曲げた時の、腕の先端の座標を取得する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function getBezierPt2( bend, len, thick ){
const joint_len = thick * abs( bend );
const v = new THREE.Vector2( len - joint_len, 0 );
const ep = v.rotateAround( center2D, lastAngle );
const cp = ep.clone();
return { ep, cp }
}
function getBezierPt2( bend, len, thick ){ const joint_len = thick * abs( bend ); const v = new THREE.Vector2( len - joint_len, 0 ); const ep = v.rotateAround( center2D, lastAngle ); const cp = ep.clone(); return { ep, cp } }
function getBezierPt2( bend, len, thick ){
    const joint_len = thick * abs( bend );
    const v = new THREE.Vector2( len - joint_len, 0 );
    const ep = v.rotateAround( center2D, lastAngle );
    const cp = ep.clone();
    return { ep, cp }
}

joint_lenはjoint部分の長さ。

joint部分は曲げ具合によって長さが変わる(曲げていない時はほぼ0)ので、全体の長さからjoint_lenを引くことで残り下腕部分の長さを出している。

ある角度aの弧の長さ = 円周*(a/PI) = (PI*直径)*(a/PI) = 直径*a

joint以降の腕は真っ直ぐに作るので、x軸方向にlenだけ伸びた真っ直ぐのベクトルを、lastAngle(=jointの最後の面の角度)だけ回転させる。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function lowerArmUpdate( angle ){
const bend = mapping( angle, 0.0, 1.5, -0.05, -3*PI/4 );
const jointArmPt = makeJointPt( upperArmObj, bend );
const { ep, cp } = getBezierPt2( bend, lowerArmLength, upperArmThick );
lowerArmObj.ep = ep;
lowerArmObj.cp = cp;
const lowerArmPt = makePipePt( lowerArmObj );
const lowerArmPts = jointArmPt.concat( lowerArmPt );
updateGeometry( jointArmObj, lowerArmPts, lowerArmGeo );
lowerArmG.position.set( upperArmObj.ep.x, upperArmObj.ep.y, 0 );
}
function lowerArmUpdate( angle ){ const bend = mapping( angle, 0.0, 1.5, -0.05, -3*PI/4 ); const jointArmPt = makeJointPt( upperArmObj, bend ); const { ep, cp } = getBezierPt2( bend, lowerArmLength, upperArmThick ); lowerArmObj.ep = ep; lowerArmObj.cp = cp; const lowerArmPt = makePipePt( lowerArmObj ); const lowerArmPts = jointArmPt.concat( lowerArmPt ); updateGeometry( jointArmObj, lowerArmPts, lowerArmGeo ); lowerArmG.position.set( upperArmObj.ep.x, upperArmObj.ep.y, 0 ); }
function lowerArmUpdate( angle ){
    const bend = mapping( angle, 0.0, 1.5, -0.05, -3*PI/4 );
    const jointArmPt = makeJointPt( upperArmObj, bend );
    const { ep, cp } = getBezierPt2( bend, lowerArmLength, upperArmThick );
    lowerArmObj.ep = ep;
    lowerArmObj.cp = cp;
    const lowerArmPt = makePipePt( lowerArmObj );
    const lowerArmPts = jointArmPt.concat( lowerArmPt );
    updateGeometry( jointArmObj, lowerArmPts, lowerArmGeo );
    lowerArmG.position.set( upperArmObj.ep.x, upperArmObj.ep.y, 0 );
}

①任意の角度angleでジョイント部の座標を作成。

②上記で作成したgetBezierPt2関数で腕の終端のポイントを取得し、makePipePtでパイプ形状の座標を作成。

①②の座標を連結し、ジオメトリーを更新。

lowerArmのグループを上側の腕の終端座標に移動。

なお、joint部とlowerArmを繋げて作成するために、以下のようにmakePipePt関数を修正した。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
unction makePipePt( obj ){
const curve = new THREE.QuadraticBezierCurve( center2D, obj.cp, obj.ep );
const bone = curve.getPoints( obj.seg );
let zpos = center2D;
const pt = [];
for( let i=0; i<( obj.seg+1 ); i++ ){
const diff = new THREE.Vector2().subVectors( bone[i], zpos );
//追加:angleの初期値をlastAngleに設定
const angle = i==0 ? lastAngle : Math.atan2( diff.y, diff.x );
pt[i] = [];
for( let j=0; j<obj.edge; j++ ){
const theta = j * 2 * PI / obj.edge;
const w = obj.thick[i] * cos( theta );
const h = obj.width[i] * sin( theta );
const v = new THREE.Vector2( 0, h );
v.add( bone[i] );
v.rotateAround( bone[i], angle );
       //追加:座標にlastPosを足す
v.add( lastPos );
pt[i][j] = [v.x, v.y, w];
}
zpos = bone[i];
lastAngle = angle;
}
//追加:座標にlastPosを足す
lastPos = bone[obj.seg].add( lastPos );
return pt;
}
unction makePipePt( obj ){ const curve = new THREE.QuadraticBezierCurve( center2D, obj.cp, obj.ep ); const bone = curve.getPoints( obj.seg ); let zpos = center2D; const pt = []; for( let i=0; i<( obj.seg+1 ); i++ ){ const diff = new THREE.Vector2().subVectors( bone[i], zpos ); //追加:angleの初期値をlastAngleに設定 const angle = i==0 ? lastAngle : Math.atan2( diff.y, diff.x ); pt[i] = []; for( let j=0; j<obj.edge; j++ ){ const theta = j * 2 * PI / obj.edge; const w = obj.thick[i] * cos( theta ); const h = obj.width[i] * sin( theta ); const v = new THREE.Vector2( 0, h ); v.add( bone[i] ); v.rotateAround( bone[i], angle );        //追加:座標にlastPosを足す v.add( lastPos ); pt[i][j] = [v.x, v.y, w]; } zpos = bone[i]; lastAngle = angle; } //追加:座標にlastPosを足す lastPos = bone[obj.seg].add( lastPos ); return pt; }
unction makePipePt( obj ){
    const curve = new THREE.QuadraticBezierCurve( center2D, obj.cp, obj.ep );
    const bone = curve.getPoints( obj.seg );
    let zpos = center2D;
    const pt = [];
    for( let i=0; i<( obj.seg+1 ); i++ ){
        const diff = new THREE.Vector2().subVectors( bone[i], zpos );
        //追加:angleの初期値をlastAngleに設定
        const angle = i==0 ? lastAngle : Math.atan2( diff.y, diff.x );
        pt[i] = [];
        for( let j=0; j<obj.edge; j++ ){
            const theta = j * 2 * PI / obj.edge;
            const w = obj.thick[i] * cos( theta );
            const h = obj.width[i] * sin( theta );
            const v = new THREE.Vector2( 0, h );
            v.add( bone[i] );
            v.rotateAround( bone[i], angle );
       //追加:座標にlastPosを足す
            v.add( lastPos );
            pt[i][j] = [v.x, v.y, w];
        }
        zpos = bone[i];
        lastAngle = angle;
    }
    //追加:座標にlastPosを足す
    lastPos = bone[obj.seg].add( lastPos );
    return pt;
}

変更点ポイント:最初の面はlastAngleの角度に傾ける、各座標にlastPos(jointの終端の位置)を加算することで、jointからパイプが生えるように位置をずらす。

腕全体の更新は、lastAngleとlastPosを初期化⇨upperArmUpdate⇨lowerArmUpdateの順番で処理する。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function armUpdate( angle1, angle2 ){
//初期化
lastAngle = 0;
lastPos = new THREE.Vector2();
//アップデート処理
upperArmUpdate( angle1 );
lowerArmUpdate( angle2 );
}
function armUpdate( angle1, angle2 ){ //初期化 lastAngle = 0; lastPos = new THREE.Vector2(); //アップデート処理 upperArmUpdate( angle1 ); lowerArmUpdate( angle2 ); }
function armUpdate( angle1, angle2 ){
    //初期化
    lastAngle = 0;
    lastPos = new THREE.Vector2();
    //アップデート処理
    upperArmUpdate( angle1 );
    lowerArmUpdate( angle2 );
}

腕完成図