three.jsで3Dキャラを作成:uvを貼る

シェーダーマテリアルを利用して、作成したメッシュにuvを貼って色を付ける。またその際、three.js側で設定したライティングを考慮した色にする。

ライトを設置

const light1 = new THREE.DirectionalLight( 0xffffff, 0.5 );
light1.position.set(0, 0, 10);
scene.add(light1);

メッシュに貼るためのテクスチャを読み込む

わかりやすいようにこんな感じの色分けした画像を用意。

画像を(2のべき乗)pxにしないとエラーが出るから注意。

const texLoader = new THREE.TextureLoader();
const armTex = texLoader.load( './data/tex/armTex.png' );

uvmapを作成

メッシュの座標とテクスチャ上の座標を対応(どこの色情報を参照するのか決める)させるため、マッピング用の配列を作る。uv座標は上の画像のようになっている。

前回まで作っていた筒状のメッシュに貼り付けることを考えたとき、基本的にはテクスチャの横をedge数、縦をsegment数(反対でもいいが)で割って、0~1の範囲をメッシュの座標に対応させるように考えればいいと思う。

こんな感じで貼りたい
function makeUvmap( obj ){
    const uvmap = [];
    for( let i=0; i<( obj.seg+1 ); i++ ){
        uvmap[i] = [];
        const y = i / obj.seg;
        for( let j=0; j<( obj.edge+1 ); j++ ){
              const x = j / obj.edge;
              uvmap[i][j] = [x, y];
        }
    }
    return uvmap;
}

あとは頂点座標の時と同じようにuv座標をジオメトリにsetAttributeしたいので、verticesと同じ体裁にuvmapの配列を整えたい。

注意なのは、verticesの方はedge数が8の時、頂点座標は以下のように端っこの部分で0に折り返している。

function setVertices( seg, edge, pt ){
    const vert = [];
    for( let i=0; i<seg; i++ ){
        vert[i] = [];
        for( let j=0; j<edge; j++ ){
            vert[i][j] = [];
            vert[i][j][0] = pt[i][j];
            vert[i][j][1] = pt[i][(j+1) % edge];
            vert[i][j][2] = pt[i+1][(j+1) % edge];
            vert[i][j][3] = pt[i+1][j];
        }
    }
    return new Float32Array( vert.flat(3) );
}

コードだと% edgeで剰余にしている部分。

セグメントごとに円を閉じるために最初の座標に戻ってくるようにしている。

でもuvは普通に(座標0地点に折り返さないで)端っこ(1.0)に行きたい。

makeUvmap関数でuvmap配列のedgeをひとつ増やしているのはそのため。

ジオメトリに対応させるため、setVertices関数と同じようにsetUvs関数を作成(edgeで折り返さないところだけ違う)。

function setUvs( seg, edge, pt ){
    const vert = [];
    for( let i=0; i<seg; i++ ){
        vert[i] = [];
        for( let j=0; j<edge; j++ ){
            vert[i][j] = [];
            vert[i][j][0] = pt[i][j];
            vert[i][j][1] = pt[i][j+1];
            vert[i][j][2] = pt[i+1][j+1];
            vert[i][j][3] = pt[i+1][j];
        }
    }
    return new Float32Array( vert.flat(3) );
}

makeGeometry関数は引数にuvmapを受け取るように追加し、uv情報をsetAttributeする記述を追加。

function makeGeometry( obj, pt, uv ){
    const vertices = setVertices( obj.seg, obj.edge, pt );
    const indices = setIndices( obj.seg, obj.edge );
    const uvs = setUvs( obj.seg, obj.edge, uv );  //←追加
    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.BufferAttribute( vertices, 3 ));
    geometry.setAttribute('uv', new THREE.BufferAttribute( uvs, 2 ));  //←追加
    geometry.setIndex(new THREE.BufferAttribute( indices, 1 ));
    const merg = new THREE.Geometry().fromBufferGeometry( geometry );
    merg.mergeVertices();
    merg.computeVertexNormals();
    return merg;
}

armInit関数内にも、uv情報を作成する記述を追加。

function armInit(){
    //  .....  ↑省略 ..... //     
    //upper arm
    const upperArmUv = makeUvmap( upperArmObj ); //uvmapを作成
    const upperArmpt = makePipePt( upperArmObj );
    upperArmGeo = makeGeometry( upperArmObj, upperArmpt, upperArmUv ); //uvmapを追加
    const upperArmMesh = new THREE.Mesh( upperArmGeo, uvMat );

    //lower arm
    const jointArmPt = makeJointPt( upperArmObj, -1 );
    const lowerArmPt = makePipePt( lowerArmObj );
    const lowerArmPts = jointArmPt.concat( lowerArmPt );
    const jointArmUv = makeUvmap( jointArmObj ); //uvmapを作成
    lowerArmGeo = makeGeometry( jointArmObj, lowerArmPts, jointArmUv ); //uvmapを追加
    const lowerArmMesh = new THREE.Mesh( lowerArmGeo, uvMat );

    //hand
    lastValClear();
    const fingerPt = makePipePt( fingerObj );
    const fingerGeo = makeGeometry( fingerObj, fingerPt, upperArmUv ); //uvmapを追加
    //  .....  ↓省略 ..... //
}

指はupperArmとseg数、edge数が一緒なので、upperArmUvをそのままmakeGeometry関数に渡している。

シェーダーマテリアルの作成

const uniform = THREE.UniformsUtils.merge([
  //ライト情報を使う設定
    THREE.UniformsLib['lights'],{
     //シェーダー側にテクスチャを渡す
        'uTexture': { value: null },
    }
]);

const material = new THREE.ShaderMaterial({
    vertexShader: document.getElementById('vert').textContent,
    fragmentShader: document.getElementById('frag').textContent,
    uniforms: uniform,
    side:THREE.DoubleSide,
    lights: true
});

const armMat = material.clone();
armMat.uniforms.uTexture.value = armTex;

シェーダーマテリアルの基本的な設定方法は以前の記事を参照。

ライティングを正しく機能させるために、uniformにTHREE.UniformsUtils.mergeを使ってlightsを読み込んでいるのと、マテリアルの設定でlightsをtrueにするのを注意する。

armMat以外にもテクスチャ画像の違うマテリアルをいくつか用意することを考えて、基本となるマテリアルをクローンして、uniformのuTexutre変数に画像情報を個別にセットしている。

シェーダー側記述

頂点シェーダー側。

ライト情報を使うため、必要ソースをincludeしているところに注意。

<script id="vert" type="x-shader/x-vertex">
    #include <common>
    #include <lights_pars_begin>
    varying vec2 vUv;
    varying vec4 fragColor;

    void main() {
        //normalはShaderMaterialで補完されるジオメトリの法線情報
        //ワールド座標系に変換
        vec3 norm = normalMatrix * normal;
        vec3 color = vec3( 0.0, 0.0, 0.0 );

        //three.jsで設置したライトの方向を取得
        vec3 vertexToLight = normalize( directionalLights[0].direction );
        //three.jsで設置したライトの色を取得
        vec3 lightCol = vec3( directionalLights[0].color );
     //法線情報とライトの方向から反射を計算(マイナスは除去)
        //ライトの色を考慮
        color = lightCol * max( dot( vertexToLight.xyz, norm ), 0.0 );

        /*
        //ライトが複数ある場合
        for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
            vec3 vertexToLight = normalize( directionalLights[i].direction );
            vec3 lightCol = vec3( directionalLights[i].color );
            color +=  lightCol * max( dot( vertexToLight.xyz, norm ), 0.0 );
        }
        */
        
        //colorをフラグメントシェーダーに渡す
        fragColor = vec4( color, 1.0 );
        //uv情報をフラグメントシェーダーに渡す
        vUv = uv;

        //positionをカメラ座標に変換
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
</script>

フラグメントシェーダー側。

<script id="frag" type="x-shader/x-fragment">
    precision mediump float;
    varying vec2 vUv;
    varying vec4 fragColor;
    uniform sampler2D uTexture;

    void main() {
        //テクスチャ画像から色を取得
        vec3 texCol = texture2D( uTexture, vUv ).rgb;
     //vec4に変換
        vec4 color = vec4( texCol, 1.0 );
     //ライティングをかける
        gl_FragColor = fragColor * color;
    }
</script>

uniformでthreejsからテクスチャ画像を受け取り、texture2Dでuv座標での色情報を取得している。

最終的にvaryingで頂点シェーダー側から受け取ったライティングの反射色と、テクスチャ画像からの色を掛け合わせている。

参考:https://qiita.com/aa_debdeb/items/870e52499dc94a2942c3

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

今回で腕は完成です。

指を追加する

const fingerObj = new Limbs();

function armInit(){
    //パラメータの設定
    const fingerLength = upperArmLength / 10;  //よさげな長さ
    const fingerThick = upperArmThick / 5;   //よさげな太さ
    const fingerThicks = new Array( limbSeg );
    for( let i=0; i<( limbSeg+1 ); i++ ){
     //太さ段々すぼめて最後は0(閉じる)
        const t = i / limbSeg;
        fingerThicks[i] = fingerThick - Math.pow( t, 3 ) * fingerThick;
    }
    fingerObj.ep = new THREE.Vector2( fingerLength,0 );
    fingerObj.cp = new THREE.Vector2( fingerLength,0 );
    fingerObj.thick = fingerThicks;
    fingerObj.width = fingerThicks;

  //meshの作成
    lastValClear();    //lastPos, lastAngleの初期化
    const fingerPt = makePipePt( fingerObj );
    const fingerGeo = makeGeometry( fingerObj, fingerPt );
  //指の立て付け
    const fingerAngles = [ -PI/4, -PI/8, 0, PI/4 ];
    const handLength = ( upperArmLength + lowerArmLength ) * 0.11;
    for( let i=0; i<4; i++ ){
        const fingerMesh = new THREE.Mesh( fingerGeo, armMat );
        const z = ( upperArmThick*0.8 ) * Math.sin( fingerAngles[i] );  //指meshのz位置
        const x = ( upperArmThick*0.8 ) * Math.cos( fingerAngles[i] );  //指meshのx位置
        fingerMesh.rotation.y = -fingerAngles[i];              //指meshを回転
        fingerMesh.position.set( x - ( handLength ), 0, z );   //handLength分だけ中に入れる
     //作成したmeshをhandGグループに追加(4つ分)
        handG.add( fingerMesh );
    }
    //lowerArmグループに追加
    lowerArmG.add( handG );
}

//lastPos, lastAngleの初期化関数
function lastValClear(){
    lastAngle = 0;
    lastPos = new THREE.Vector2();
}

指の形状のメッシュを4個分作る。

fingerAnglesの角度に指をくっつける。

なお、指はちっちゃく作るので、特に曲げたりは想定してない。

腕のジオメトリを更新する関数(armUpdate)の最後に、handG(指のグループ)の座標と角度を更新する処理を入れる。

function armUpdate( angle1, angle2 ){
    //upperArm
    lastValClear();
    upperArmUpdate( angle1 );
    //lowerArm
    lowerArmUpdate( angle2 );
    //hand
    handG.rotation.z = lastAngle;    //角度を更新
    handG.position.set( lastPos.x, lastPos.y, 0 );  //位置を更新
}

回転を追加する

armupdate関数に処理を追加して、腕全体(armG)と、ひじから先(lowerArmG)を個別に回転できるようにする。

function armUpdate( angle1, angle2, rotate1, rotate2 ){
    //upperArm
    lastValClear();
    upperArmUpdate( angle1 );
    //lowerArm
    const r = lastAngle;     //upperArmの最終の角度をとっておく
    lowerArmUpdate( angle2 );
    //hand
    handG.rotation.z = lastAngle;
    handG.position.set( lastPos.x, lastPos.y, 0 );
    //回転の処理
    armG.quaternion.set( 0,0,0,1 );
    lowerArmG.quaternion.set( 0,0,0,1 );
    const axis1 = new THREE.Vector3( 1,0,0 );
    const axis2 = new THREE.Vector3( Math.cos(r),Math.sin(r),0 ).normalize();
    const q1 = new THREE.Quaternion().setFromAxisAngle( axis1, rotate1 );
    const q2 = new THREE.Quaternion().setFromAxisAngle( axis2, rotate2 );
    armG.applyQuaternion( q1 );
    lowerArmG.applyQuaternion( q2 );
}

回転にはクオータニオンを使う。クオータニオンの細かいことは分からないが、とりあえず任意の軸にそってメッシュを回転させる、というのが実現できればよい。

まずそれぞれメッシュの回転を初期化。

armG.quaternion.set( 0,0,0,1 );
lowerArmG.quaternion.set( 0,0,0,1 );

次に回転させたい軸の設定をする。腕全体はx軸にそって回転させればいいので、axis1に(1,0,0)のベクトルを設定。

ひじから先はupperArmの曲がっている方向を軸にして回転させたいので、upperArmの最後の角度からベクトルを計算する。

const axis1 = new THREE.Vector3( 1,0,0 );
const axis2 = new THREE.Vector3( Math.cos(r),Math.sin(r),0 ).normalize();

軸に設定するベクトルは正規化されている必要があるので注意。

最後に、setFromAxisAngleメソッドでそれぞれの軸と回転させたい角度を設定し、メッシュのグループに適用する。

const q1 = new THREE.Quaternion().setFromAxisAngle( axis1, rotate1 );
const q2 = new THREE.Quaternion().setFromAxisAngle( axis2, rotate2 );
armG.applyQuaternion( q1 );
lowerArmG.applyQuaternion( q2 );

アニメーション

アニメーションループに腕の回転の動きも追加する。

armGの回転はrotVal1、lowerArmGの回転はrotVal2に値の推移をセットしている。

アニメーションの値を書き込むようのオブジェクトの、positionのプロパティには腕の曲げ角度の方を入れているので、回転はscaleのプロパティを使うことにした。

それぞれのキーフレームを配列としてAnimationClipに渡すことで、同時に曲げと回転の値をアニメーションできる。

const upperArmMove = new THREE.Object3D();
//armUpdateに渡すangleとrotateの値をセット
const dur = [ 0, 2, 4 ];
const posVal1 = [ -0.5, 0, -0.5 ];
const posVal2 = [ 0, 1.5, 0 ];
const rotVal1 = [ 0, 0, 0 ];
const rotVal2 = [ 0, -PI, 0 ];
//配列の形にする
const upperArmPos = [];
const upperArmRot = [];
for( let i=0; i<dur.length; i++ ){
    upperArmPos.push( posVal1[i] );
    upperArmPos.push( posVal2[i] );
    upperArmPos.push( 0 );
    upperArmRot.push( rotVal1[i] );
    upperArmRot.push( rotVal2[i] );
    upperArmRot.push( 0 );
}

//それぞれのキーフレームを作りclipに配列として追加
const uppperArmKF1 = new THREE.NumberKeyframeTrack( '.position', dur, upperArmPos );
const uppperArmKF2 = new THREE.NumberKeyframeTrack( '.scale', dur, upperArmRot );
const clip = new THREE.AnimationClip( 'Action', 4, [ uppperArmKF1, uppperArmKF2 ] );

レンダリングサイクル内でarmUpdate関数を呼び出す。

const clock = new THREE.Clock();
render();
function render(){
    //animation update
    mixer.update(clock.getDelta());
    let angle1 = upperArmMove.position.x;
    let angle2 = upperArmMove.position.y;
    let rot1 = upperArmMove.scale.x;
    let rot2 = upperArmMove.scale.y;
    armUpdate( angle1, angle2, rot1, rot2 );

    //cycle
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

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

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

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

パラメータの設定

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

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にすることで先端が閉じたチューブの形にする。

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

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

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から下(今後指もここに追加)

//グループを作る
const armG = new THREE.Group();
const lowerArmG = new THREE.Group();

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

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

曲げてみる

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

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の最後の面の角度)だけ回転させる。

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関数を修正した。

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の順番で処理する。

function armUpdate( angle1, angle2 ){
    //初期化
    lastAngle = 0;
    lastPos = new THREE.Vector2();
    //アップデート処理
    upperArmUpdate( angle1 );
    lowerArmUpdate( angle2 );
}

腕完成図

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

ひじの作成

前回作成した上腕に繋がる関節(ひじ)部分を作る。

完成イメージ

ボーンを作成する

makePipeptと同様に、まずボーンの座標を計算する。

//make origin
const radius = upperArmObj.thick[0];
const origin = new THREE.Vector2( 0,-radius );
origin.rotateAround( center2D, lastAngle );

radiusは円の半径(関節の太さ)。上腕の太さから取得。均一の太さにするので配列にしない。

次に、ボーンベクトルを回転させる際の中心点となるoriginを取得する。

originはupperArmを真横から見た時に、先端の一番下側の点になる(下の図参照)

上腕の最後の角度によってorigin座標が変わるので、グローバルにlastAngle変数を用意して、makePipePt関数内で最後の円の角度をlastAngle変数に格納するようにした。

originを中心としてboneベクトルを回転させることで各ボーンの座標を取得する。

const bone = new THREE.Vector2();
const pt = [];
for( let i=0; i<( seg+1 ); i++ ){
    pt[i] = [];
    let angle = i==0 ? 0 : bend / obj.seg;
    bone.rotateAround( origin, angle );
    for( let j=0; j<edge; j++ ){
        const theta = j * 2 * PI / edge;
        const w = radius * cos( theta );
        const h = radius * sin( theta );
        const v = new THREE.Vector2( 0, h );
        v.add( bone );
        v.rotateAround( bone, i * angle + lastAngle );
        pt[i][j] = [ v.x, v.y, w ];
    }
}

angleにボーンのベクトルを回転させる角度をセットし、originを中心にしてセグメントごとに回転させる。

あとはmakePipePtと同じ要領で、各ボーンを中心にした円の座標を計算している。

関数化したものが以下。

function makeJointPt( obj, bend ){
    //make origin
    const radius = obj.thick[0];
    const origin = new THREE.Vector2( 0,-radius );
    origin.rotateAround( center2D, lastAngle );
    //set pt
    const bone = new THREE.Vector2();
    const pt = [];
    for( let i=0; i<( obj.seg+1 ); i++ ){
        pt[i] = [];
        let angle = i==0 ? 0 : bend / obj.seg;
        bone.rotateAround( origin, angle );
        for( let j=0; j<obj.edge; j++ ){
            const theta = j * 2 * PI / obj.edge;
            const w = radius * cos( theta );
            const h = radius * sin( theta );
            const v = new THREE.Vector2( 0, h );
            v.add( bone );
            v.rotateAround( bone, i * angle + lastAngle );
            pt[i][j] = [ v.x, v.y, w ];
        }
    }
    //update values
    lastAngle += bend;  //lastAngleの値を更新する。
    lastBonePos = bone; //boneの終端の値をlastBonePosに格納する。
    return pt;
}

メッシュの作成

function jointInit(){
    const pt = makeJointPt( upperArmObj, -0.05 );
    jointArmGeo = makeGeometry( upperArmObj, pt );
    jointArmMesh = new THREE.Mesh( jointArmGeo, upperArmMat );
    scene.add( jointArmMesh );
}

makeJointPtで座標を取得、この時角度が0だとfacelessなmeshになってしまいエラーが出るので、とりあえず小さい数字を渡している。

ジオメトリとメッシュを作成してシーンに追加。必要パラメータがupperArmと一緒なのでとりあえずupperArmObjを渡しているが、別途jointArmObjを用意してもよいのかも。

アニメーションする

function jointArmUpdate( angle ){
    const bend = mapping( angle, 0.0, 1.5, -0.01, -3*PI/4 );
    const pt = makeJointPt( upperArmObj, bend );
    updateGeometry( upperArmObj, pt, jointArmGeo );
    jointArmMesh.position.set( upperArmObj.ep.x,  upperArmObj.ep.y, 0 );
}

0(曲げない)~1.5(最大限曲げる)の間で角度をマッピングし、makeJointPtで新しい座標を取得、updateGeometryでジオメトリを更新する。

upperArmの先端があるポイントにひじのメッシュの初期位置を移動させる。

function limbupdate( bend1, bend2 ){
    lastAngle = 0;
    lastPos = new THREE.Vector2();
    upperArmUpdate( bend1 );
    jointArmUpdate( bend2 );
};

upperArmUpdate⇒jointUpdateの順番でジオメトリを更新する関数を作成。

ジオメトリをアップデートする関数の最初にlastAngleとlastPosを初期化する処理を入れておく。

(※これがないとアニメーションループごとに値が増えていってしまう)

キーフレームの作成

const upperArmMove = new THREE.Object3D();
const dur = [ 0, 2, 4 ];
const val1 = [ 2, -1, 2 ];
const val2 = [ 0, 1.5, 0 ];

const upperArmPos = [];
for( let i=0; i<dur.length; i++ ){
    upperArmPos.push( val1[i] );
    upperArmPos.push( val2[i] );
    upperArmPos.push( 0 );
}

const uppperArmKF = new THREE.NumberKeyframeTrack( '.position', dur, upperArmPos );

今回はupperArmとJointの両方を同時に動かすため、ダミーで作成したObject3DのuserDataではなく、positionをパラメータとして使う。

positionを使うことでx, y, zの3つのパラメータが同時に設定できる。

position.xにupperArmの曲がり具合を、position.yにJointの曲がり具合を決める値を入れている。

function render(){
    //animation update
    mixer.update(clock.getDelta());
    let angle1 = upperArmMove.position.x;
    let angle2 = upperArmMove.position.y;
    limbupdate( angle1, angle2 );
    //cycle
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

レンダリングサイクル内で、positionからパラメータをそれぞれ取得し、limbUpdateでメッシュのジオメトリを更新する。

動いているところは以下のようになる。

(境目が気になるので、個別にメッシュを作成⇒くっつける、ではなく1個のメッシュにしようかなとおもう。。)

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

今回から本格的にキャラクターの形を作っていく。

まず前回までで作ったチューブ状の形を腕っぽく曲げてアニメーションするための仕組みを作る。

腕を上下させる角度を決定する

以下のような腕の可動域を考えて、ボーンの終端座標を角度から計算する。

const v = 1;
const bend = mapping( v, -1.0, 2.0, PI/4, -PI/2 );
function mapping( inVal, inMin, inMax, outMin, outMax ){
    const ratio = ( inVal - inMin ) / ( inMax - inMin );
    const outVal = ratio * ( outMax - outMin ) + outMin;
    return outVal;
};

アニメーションを付けるときに指定しやすいよう(角度をいちいち指定するのは面倒なので)、まず上記のようなmapping関数を定義してv = -1~2の値から角度を取れるようにした。

次に、腕の長さmapping関数で取得した角度から、ベジエ曲線の終端とコントロール点の座標を計算する。

const { ep, cp } = getBezierPt( armLength, bend );
function getBezierPt( len, bend ){
    const ep_x = len * cos( bend );
    const ep_y = len * sin( bend );
    const cp_x = abs( ep_y / 2 );
    const cp_y = Math.max( ep_y / 2, 0 );
    const ep = new THREE.Vector2( ep_x, ep_y );
    const cp = new THREE.Vector2( cp_x, cp_y );
    return { ep, cp }
}

cpのx座標は、(T字ポーズをしている時が腕の曲げが最小であるとして)腕の曲げが上下どちらの方向にも大きい時に大きくする。

cpのy座標は、腕を上に曲げた時だけ上方向に大きくする。

この時戻り値と受け取る側の変数名(ep, cp)が一致している必要がある(連想配列を渡しているのでキー名は任意にできない)ので注意。

upperArmのメッシュを生成

上腕パーツの初期状態(横にまっすぐ伸びている状態)のメッシュを生成してシーンに追加するまでの工程をupperArmInit関数として定義。

前回記事と同様に、手足オブジェクト(Limbs)のインスタンスにパラメータを設定する。

ジオメトリはアニメーション時に更新するためグローバル変数として定義しておく。(初回makeGeometry作成時に再代入されるのでletで定義)

//ジオメトリはmakeGeometry呼び出しで再代入される
let upperArmGeo;

//Limbsクラスのインスタンス化
const upperArmObj = new Limbs();

//upperArmを初期化して生成
upperArmInit();
function upperArmInit(){
    //太さをセット
    const upperArmThicks = new Array( limbSeg );
    for( let i=0; i<( limbSeg+1 ); i++ ){
        upperArmThicks[i] = upperArmThick;
    }
    //各パラメータをセット
    upperArmObj.ep = new THREE.Vector2( upperArmLength,0 );
    upperArmObj.thick = upperArmThicks;
    upperArmObj.width = upperArmThicks;
    //メッシュを作成しシーンに追加
    const pt = makePipePt( upperArmObj );
    upperArmGeo = makeGeometry( upperArmObj, pt );
    const mesh = new THREE.Mesh( upperArmGeo, upperArmMat );
    scene.add( mesh );
 }

アニメーションする

アニメーションシステムを使って、値が4秒かけて-1→2→-1とリニアに変化するアニメーションを作成。

const upperArmMove = new THREE.Object3D();
const dur = [ 0, 2, 4 ];
const val = [ -1, 2, -1 ];
const uppperArmKF = new THREE.NumberKeyframeTrack( '.userData', dur, val );
const clip = new THREE.AnimationClip( 'Action', 4, [ uppperArmKF ] );
const mixer = new THREE.AnimationMixer( upperArmMove );
const clipAction = mixer.clipAction( clip );
clipAction.play();

引数angleに指定した値からep,cpを求め、ジオメトリを更新する関数を定義。

function armUpdate( angle ){
    const bend = mapping( angle, -1.0, 2.0, PI/4, -PI/2 );
    const { ep, cp } = getBezierPt( upperArmLength, bend );
    upperArmObj.ep = ep;
    upperArmObj.cp = cp;
    const pt = makePipePt( upperArmObj );
    updateGeometry( upperArmObj, pt, upperArmGeo );
}

レンダリングサイクル内でarmUpdateを呼び出してジオメトリを更新する。

const clock = new THREE.Clock();
let armAngle;
render();
function render(){
    //アニメーションのアップデート
    mixer.update(clock.getDelta());
    armAngle = upperArmMove.userData;
    limbUpdate( armAngle );
    //次のサイクル呼び出し
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

腕を上げ下げしているイメージの、このようなアニメーションが完成。

three.jsでカスタム図形を作る(4):アニメーションを付ける

前回の記事までで曲がるチューブ状のオブジェクトを作ったが、実は自前で計算しなくてもthree.jsにはSkinnedMeshという便利なオブジェクトがあり、ボーンによるアニメーションができるみたいだった。

でもアニメーション時に各ボーンの位置等をスクリプトで設定するのもそれはそれで大変かなという予感がしたので、とりあえずこのまま突き進みたいと思う。

パーツをクラス化?する

のちのち使いまわして手足に拡張することを考えて、Limbsのクラス的なものを作成して共通パラメータを吸収する。

ここではまず腕の関節から上をイメージしてupperArmパーツを作成する。

//limbsクラス
const limbSeg = 5;  //任意の値:Limbsパーツ共通
const limbEdge = 12; //任意の値:Limbsパーツ共通
function Limbs(){
    this.seg = limbSeg; 
    this.edge = limbEdge;
    this.ep = new THREE.Vector2( 1,0 );
    this.cp = new THREE.Vector2( 1,0 );
    this.thick = thick;
    this.width = width;
 }

//upperarm太さの設定
const upperArmThick = new Array( limbSeg );
for( let i=0; i<( limbSeg+1 ); i++ ){
    upperArmThick[i] = 5; //segmentごとの腕の太さ
}

//upperarmのパラメータ設定
const upperArm = new Limbs(
    new THREE.Vector2( 10, 0 ),
    new THREE.Vector2( 8, 0 ),
    upperArmThick, //縦の太さ
    upperArmThick  //横の太さ
);

//meshを作成
const upperArmPt = makePipePt( upperArm );
const upperArmGeo = makeGeometry( upperArm, upperArmPt );
const upperArmMat = new THREE.MeshNormalMaterial({
    side:THREE.DoubleSide,
});
const upperArmMesh = new THREE.Mesh( upperArmGeo, upperArmMat );
scene.add( upperArmMesh );

Limbクラスのep, cpは0だと面の存在しない図形になってしまいエラーがでるので、適当な値をセットしておいた。

makePipePt、makeGeometryの関数はオブジェクトを渡す形式に変更して以下のように修正した。

function makePipePt( obj ){
    //make bone
    const curve = new THREE.QuadraticBezierCurve( center2D, obj.cp, obj.ep );
    const bone = curve.getPoints( obj.seg );
    let zpos = center2D;
    //set points
    const pt = [];
    for( let i=0; i<( obj.seg+1 ); i++ ){
        //calc angle
        const diff = new THREE.Vector2().subVectors( bone[i], zpos );
        const angle = Math.atan2( diff.y, diff.x );
        //calc coords
        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 );
            pt[i][j] = [v.x, v.y, w];
        }
        zpos = bone[i];
    }
    return pt;
}

また、makeGeometry内でジオメトリをマージして表面を滑らかに表示させるようにする処理を追加している。

function makeGeometry( obj, pt ){
  const vertices = setVertices( obj.seg, obj.edge, pt );
    const indices = setIndices( obj.seg, obj.edge );
    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position', new THREE.BufferAttribute( vertices, 3 ));
    geometry.setIndex(new THREE.BufferAttribute( indices, 1 ));
    //マージ処理を追加
    const merg = new THREE.Geometry().fromBufferGeometry( geometry );
    merg.mergeVertices();
    merg.computeVertexNormals();
    return merg;
}

アニメーションの設定

THREE.jsのAnimationシステムを利用して、任意の時間で補間された値を取得する。

//①アニメーション用に空のオブジェクトを作成
const upperArmMove = new THREE.Object3D(); 

//②キーフレームの作成
const val = [ 0, 10 ]; //数値の変化
const dur = [ 0, 3 ];  //数値が変化する時間
const uppperArmKF = new THREE.NumberKeyframeTrack( '.userData', dur, val );

//③キーフレームをアニメーションクリップに登録
const clip = new THREE.AnimationClip( 'Action', 3, [ uppperArmKF ] );

//④mixerの設定
const mixer = new THREE.AnimationMixer( upperArmMove );
const clipAction = mixer.clipAction( clip );

//⑤アニメーション開始
clipAction.play(); 

①THREE.jsのAnimationシステム本来オブジェクトのプロパティ(position等)に設定してアニメーションさせる仕組みになっているが、今回は直接オブジェクトを動かすのではなく値だけがほしいので、最初に空のオブジェクト(upperArmMove)を作成している。

②durの時間をかけてvalに変化するキーフレームを作成、①のオブジェクトのuserDataプロパティと紐づける。

③作成したキーフレームをアニメーションクリップに登録。この時別の複数のキーフレームを配列で登録することが可能。(同じオブジェクトに対して複数のプロパティの値を変化させられる)

④ミキサーを作成してオブジェクトとクリップに紐づけ。

⑤アニメーションを開始させる。

レンダリングサイクルでアニメーションをアップデートする

const clock = new THREE.Clock();
render();
function render(){
    //アニメーションのアップデート
    mixer.update(clock.getDelta());
    const v = upperArmMove.userData;
    upperArm.ep = new THREE.Vector2( 10, v );
    const upperArmPt2 = makePipePt( upperArm );
    updateGeometry( upperArm, upperArmPt2, upperArmGeo );
    //ループ
    requestAnimationFrame(render);
    renderer.render(scene, camera);
}

レンダーサイクルの中でmixerをアップデート(これをしないと数値が変化しない)。

mixerはupperArmMoveのuserDataプロパティを変化させるよう紐づけられているので、これを参照してupperArmのプロパティであるepのy軸を変化させている。

変更した値でmakePipePt関数で座標を再計算し取得する。

さらに、取得した座標でジオメトリを更新する関数を呼び出し。関数の内容は以下。

function updateGeometry( obj, pt, geometry ){
    const indices = setIndices( obj.seg, obj.edge );
    const vertices = setVertices( obj.seg, obj.edge, pt );
    const newGeo = new THREE.BufferGeometry();
    newGeo.setAttribute('position', new THREE.BufferAttribute( vertices, 3 ));
    newGeo.setIndex(new THREE.BufferAttribute( indices, 1 ));
    const merg = new THREE.Geometry().fromBufferGeometry( newGeo );
    merg.mergeVertices();
    geometry.vertices = merg.vertices;
    geometry.elementsNeedUpdate = true;
    geometry.computeVertexNormals();
}

引数で渡された新しい座標(pt)でBufferGeometryを作成⇒マージ⇒元のジオメトリに代入⇒ノーマルの再計算。

という流れ。elementsNeedUpdate = trueの設定はレンダリングごとに必要(毎回自動的にfalseになるらしい)。

これで、3秒かけてチューブの端のy座標が0から10に変化するアニメーションが出来た。

アニメーションのようす

three.jsでカスタム図形を作る(3):曲がるパイプを作る

ボーンの作成

ボーンに沿って曲がるようなチューブ状のメッシュを作成する。

ボーンとなるベジエ曲線を作成し、セクション数ごとに分割した地点の座標を取得する。

//start、end、controlポイントの座標作成
const center2D = new THREE.Vector2();  
const ep = new THREE.Vector2( 10,20 );
const cp = new THREE.Vector2( 10,0 );
//ベジエ曲線を作成
const curve = new THREE.QuadraticBezierCurve(center2D, cp, ep);
//セクション数に分割した座標の配列を取得
const sect = 4;
const bone = curve.getPoints(sect);
bone配列を作成

※sect数4でgetPointsするとbone配列のサイズは5になる。

分割したボーン間の角度を求める

let zpos = bone[0];
for(let i=0; i<(sect+1); i++){
    //zpos(bone[n-1])とbone[n]の角度を求める
    const diff = new THREE.Vector2().subVectors(bone[i], zpos);
    const angle = Math.atan2(diff.y, diff.x);
}

ボーンに沿った円の座標を取得

const edge = 12;
const size = 5;
const pt = [];
for(let i=0; i<(sect+1); i++){
    //上記angleを求める式(省略)
    pt[i] = [];
    for(let j=0; j<edge; j++){
        const theta = j * 2 * Math.PI / edge;
        const w = size * Math.cos(theta);  //奥行(z軸上の幅)
        const h = size * Math.sin(theta);  //縦
        const v = new THREE.Vector2(0, h); //回転させるためベクトルに変換
        v.add(bone[i]);                    //boneを中心に移動
        v.rotateAround(bone[i], angle);    //angle分回転
        pt[i][j] = [v.x, v.y, w];
    }
}

z-y平面上に半径=sizeの円を描き、z軸で回転させているイメージ。

この時、boneの座標を加算して、各円の回転の中心をboneに持ってきている。

メッシュの作成

前回の記事と同じく、BufferGeometryを使ってジオメトリを作成する。

以下は関数化してまとめたコードの全体。

//ページの読み込みを待つ
window.addEventListener('load', init);
function init() {

    ///////////////////////////////////////////////
    //           画面設定                   //
    //////////////////////////////////////////////

    //画面サイズを指定
    const width = window.innerWidth;
    const height = window.innerHeight;
    //レンダラーを作成
    const renderer = new THREE.WebGLRenderer({
        canvas: document.querySelector('#myCanvas')
    });
    renderer.setClearColor(new THREE.Color('grey'));
    document.body.appendChild( renderer.domElement );
    renderer.setSize(width, height);
    //シーンを作成
    const scene = new THREE.Scene();
    //カメラを作成
    const camera = new THREE.PerspectiveCamera(45, width / height);
    camera.position.set(-50, 50, 100);
    camera.lookAt(new THREE.Vector3(0, 0, 0));
    //ライトを設置
    const envlight = new THREE.AmbientLight(0xffffff, 1);
    scene.add(envlight);

    ///////////////////////////////////////////////
    //         マテリアル作成                  //
    ///////////////////////////////////////////////

  //パイプの設定
    const sect = 5;
    const edge = 12;
    const size = 5;
    const center2D = new THREE.Vector2();
    const ep = new THREE.Vector2( 10, 10 );
    const cp = new THREE.Vector2( 10, 0 );
    //パイプを作成
    const pt = makePipe(sect, edge, size, cp, ep);
  //メッシュの作成
    const geometry = makeGeometry(sect, edge, pt);
    const material = new THREE.MeshNormalMaterial({
        side:THREE.DoubleSide,
    });
    const plane = new THREE.Mesh( geometry, material );
    scene.add( plane );

    ///////////////////////////////////////////////
    //            共通関数                  //
    ///////////////////////////////////////////////

    //パイプ作成
    function makePipe(sect, edge, size, cp, ep){
        //make bone
        const curve = new THREE.QuadraticBezierCurve(center2D, cp, ep);
        const bone = curve.getPoints(sect);
        let zpos = bone[0];
        //set points
        const pt = [];
        for(let i=0; i<(sect+1); i++){
            //calc angle
            const diff = new THREE.Vector2().subVectors(bone[i], zpos);
            const angle = Math.atan2(diff.y, diff.x);
            //calc coords
            pt[i] = [];
            for(let j=0; j<edge; j++){
                const theta = j * 2 * Math.PI / edge;
                const w = size * Math.cos(theta);
                const h = size * Math.sin(theta);
                const v = new THREE.Vector2(0, h);
                v.add(bone[i]);
                v.rotateAround(bone[i], angle);
                pt[i][j] = [v.x, v.y, w];
            }
            zpos = bone[i];
        }
        return pt;
    }

  //カスタムvertexの作成
    function setVertices(sect, edge, pt){
        const vert = [];
        for(let i=0; i<sect; i++){
            vert[i] = [];
            for(let j=0; j<edge; j++){
                vert[i][j] = [];
                vert[i][j][0] = pt[i][j];
                vert[i][j][1] = pt[i][(j+1)%edge];
                vert[i][j][2] = pt[i+1][(j+1)%edge];
                vert[i][j][3] = pt[i+1][j];
            }
        }
        return new Float32Array(vert.flat(3));
    }

  //カスタムindexの作成
    function setIndices(sect, edge){
        const num_rect = sect * edge;
        const order = [0,3,2,2,1,0];
        const index = [];
        for(let i=0; i<num_rect; i++){
            for(let j=0; j<order.length; j++){
                index.push(order[j]+(4*i));
            }
        }
        return new Uint16Array(index);
    }

    //BufferGeometryの作成
    function makeGeometry(sect, edge, pt){
      const vertices = setVertices(sect, edge, pt);
      const indices = setIndices(sect, edge);
      const geometry = new THREE.BufferGeometry();
      geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
      geometry.setIndex(new THREE.BufferAttribute(indices, 1));
      geometry.computeVertexNormals();
      return geometry;
    }

    ///////////////////////////////////////////////
    //         レンダリング開始               //
    //////////////////////////////////////////////

    render();
    function render(){
        requestAnimationFrame(render);
        renderer.render(scene, camera);
    }
}

AR.js+three.jsでwebAR(1)

blenderで作成した3DモデルをARで表示するまで。

①blenderでモデルをglbとしてエクスポート

②three.jsのGLTFLoaderでモデルを読み込み

③AR.jsでモデルを表示

という流れ。

blenderでモデルをエクスポート

フォーマットはglTF Binary(.glb)が一番軽いのでこれを選択。

パラメータはとりあえず以下のチェックが入っていればテクスチャ等もそのまま出力される様子。

※この時マテリアルをDeffuse BSDFにしたところブラウザの方でうまくテクスチャが反映されなかったので、Principled BSDFのままのほうがいいっぽい。

html側の記述

<!DOCTYPE html>
<html>
    <head>
        <title>AR test</title>
        <meta name="viewport" content="width=device-width, user-scalable=no"/>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r123/three.min.js"></script>
        <script src="https://raw.githack.com/AR-js-org/AR.js/3.1.0/three.js/build/ar.js"></script>
        <script src="https://cdn.jsdelivr.net/gh/mrdoob/three.js@r92/examples/js/loaders/GLTFLoader.js"></script>
        <script src="three.js"></script>
    </head>
    <body style='margin : 0px; overflow: hidden;'></body>
</html>

・ビューポートの設定。画面をデバイスの大きさに合わせる。またスマホのズーム操作を禁止

・必要ソースの読み込み

・bodyタグで余白の除去とスクロールの禁止

three.js側の記述

まずARの表示に最低限必要な画面設定のみ

window.addEventListener("DOMContentLoaded", init);

function init() {
    //レンダラーの設定
    const renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true
    });
    renderer.setClearColor(new THREE.Color(), 0);
    renderer.setSize(640, 480);
    renderer.domElement.style.position = 'absolute';
    renderer.domElement.style.top = '0px';
    renderer.domElement.style.left = '0px';
    document.body.appendChild(renderer.domElement);
   
    //画面設定
    const scene = new THREE.Scene();
    scene.visible = false;
    const camera = new THREE.Camera();
    scene.add(camera);
    const light = new THREE.AmbientLight(0xFFFFFF, 1.0);
    scene.add(light);

    //画面リサイズの設定
    window.addEventListener('resize', () => {
        onResize();
    });
    function onResize() {
        arToolkitSource.onResizeElement();
        arToolkitSource.copyElementSizeTo(renderer.domElement);
        if (arToolkitContext.arController !== null) {
            arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
        }
    };
      
    //AR周りの設定
    const arToolkitSource = new THREEx.ArToolkitSource({
        sourceType: 'webcam'
    });
    arToolkitSource.init(() => {
        setTimeout(() => {
            onResize();
        }, 2000);
    });
    const arToolkitContext = new THREEx.ArToolkitContext({
        cameraParametersUrl: 'data/camera_para.dat',
        detectionMode: 'mono'
    });
    arToolkitContext.init(() => {
        camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
    });
    
    //マーカー設定  
    const marker1 = new THREE.Group();
    scene.add(marker1);
    const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, marker1, {
        type: 'pattern',
        patternUrl: 'data/patt.hiro',
    });

    //レンダリング
    requestAnimationFrame(function animate(){
        requestAnimationFrame(animate);
        if (arToolkitSource.ready) {
            arToolkitContext.update(arToolkitSource.domElement);
            scene.visible = camera.visible;
        }
        renderer.render(scene, camera);
    });
}

arToolkitSource初期化時のリサイズ処理は2秒待つ処理を入れないと自分の環境ではうまくいかなかった(公式のexsampleでは意図的に待つ処理を入れている)。

モデルの読み込みと表示

GLTFローダーでモデルを読み込み、マーカーに対応させることで表示

      const gltfloader = new THREE.GLTFLoader();
      gltfloader.load('./data/apple.gltf',function(gltf){
          marker1.add(gltf.scene);
      });

・’./data/apple.gltf’はモデルデータのURL

・モデルそのものではなくシーンを追加していることに注意

three.jsでカスタム図形を作る(2)

前回シンプルな四角い板を作ったので、今回はそれを応用して多角柱を作ってみる。

作りたい形のイメージ(六角柱)

①図形の座標を計算する

多角形を作り、セクションの数ぶん画面の奥に伸ばしていくイメージ。

ここでは六角形を2セクションぶんの奥行きで作ってみることにする。

  const sect = 2; //sectionの数
  const edge = 6; //edgeの数
  const size = 10; //1辺の長さ
  
  const pt = []; //座標を入れる配列
  for(let i=0; i<(sect+1); i++){
      pt[i] = [];
      let z = -size * i; //奥行き
          for(let j=0; j<edge; j++){
              let theta = j*2*Math.PI / edge;
              let x = size * Math.cos(theta);
              let y = size * Math.sin(theta);
              pt[i][j] = [x, y, z];
      }
  }

座標はptに下図のような順番で入る。(pt[2][0]以降は省略)

②verticesの作成

座標がこの順番のままだと面が貼れないので、四角のブロックごとに区切っていくように座標の順番を変えてverticesを作る。

  const vert = [];
  for(let i=0; i<sect; i++){
      vert[i] = [];
      for(let j=0; j<edge; j++){
          vert[i][j] = [];
          vert[i][j][0] = pt[i][j];
          vert[i][j][1] = pt[i][(j+1)%edge]; //edgeを一周回ったら0に戻る
          vert[i][j][2] = pt[i+1][(j+1)%edge];
          vert[i][j][3] = pt[i+1][j];
      }
  }
  const vertices = new Float32Array(vert.flat(3)); //1次元配列に変換

下図のような順番に座標の順番を整理した後でverticesを作成している。

③indicesの作成

面を貼る順番が表側になるように(前回の記事参照)ひとつめの四角(0⇒3⇒2⇒2⇒1⇒0)ふたつめの四角(4⇒7⇒6⇒6⇒5⇒4)の要領で四角の数ぶんindexの順番を入れていく。

  const num_rect = sect * edge;
  const order = [0,3,2,2,1,0];
  const index = [];
  for(let i=0; i<num_rect; i++){
      for(let j=0; j<order.length; j++){
          indx.push(order[j]+(4*i));
    }
  }
  const indices = new Uint16Array(index);

④バッファーオブジェクトの作成

頂点座標とインデックスをアトリビュートするところは前回と一緒だが、さらにcomputeVertexNormals()で法線向きを計算してあげる必要がある。

 const geometry = new THREE.BufferGeometry();
  geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));
  geometry.computeVertexNormals();

⑤メッシュを作成

あとは任意のマテリアルでメッシュを作成する。

結果が分かりやすいようにノーマルが色付けされるマテリアルを使って、ワイヤーフレーム表示させてみる。

  const material = new THREE.MeshNormalMaterial({
    side:THREE.DoubleSide, //面の裏側も表示する
    wireframe:true
  });
  const plane = new THREE.Mesh( geometry, material );

  scene.add( plane );

結果はこんな感じ。(正面からだと図形がわかりにくいのでカメラのポジションをちょっとずらしている)

さらにedgeの数を増やせば円錐に近づく。(カメラ左から見るとノーマルの色が変わる)

three.jsでカスタム図形を作る(1)

自分で座標を自由に作って図形を表示させる時のやり方。

先ずはシンプルな四角い板を作ってみる。

①verticesの作成

それぞれの頂点のx,y,z座標を配列に詰める。

  const positions = [
        5.0,  5.0, 0.0, //頂点0
        5.0, -5.0, 0.0, //頂点1
       -5.0, -5.0, 0.0, //頂点2
       -5.0,  5.0, 0.0  //頂点3
  ];
  const vertices = new Float32Array(positions);

②indicesの作成

次に、さっき指定した座標に面を貼っていく。面は三角形単位で貼る必要がある。この時、三角形を貼る順番によって法線の向きが変わる。反時計回りに貼った面は表側になり、その逆だと面が裏側になる。

表側の面を貼りたいので、頂点0⇒頂点3⇒頂点2、頂点2⇒頂点1⇒頂点0のように三角形の面を貼る順番をindices配列に入れていく。

  const order = [0,3,2,2,1,0];
  const indices = new Uint16Array(order);

③バッファーオブジェクトの作成

バッファーオブジェクトを作成し、さっき作った頂点座標とインデックス情報を与える。

  const geometry = new THREE.BufferGeometry();
  geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
  geometry.setIndex(new THREE.BufferAttribute(indices, 1));

positionにはvertices配列のデータが3つずつ入り、index番号に対応した順番で面が作られる。

④メッシュを作成してシーンに追加

あとは好きなマテリアルを選んでメッシュを作成してシーンに表示させることができる。

  const material = new THREE.MeshLambertMaterial({color: 0x6699FF});
  const plane = new THREE.Mesh( geometry, material );
  scene.add( plane );