2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OthloTechAdvent Calendar 2018

Day 6

OpenGLを用いて3次元上でリップシンク(口パク)を行う

Last updated at Posted at 2018-12-05

初めまして。私、Shibataというものです。
主にc++とOpenGLを好んでいます。と言っても、レベルは学校の授業の範疇程です。

↓学校の課題では
ezgif.com-video-to-gifのコピー3.gif
↓このようなものばかり作っています。
ezgif.com-video-to-gifのコピー2.gif

まず大前提として、私は大変プログラミングの知識が浅いペーペーなため、
便利なプログラムや画期的アイデアを紹介するのではなく
私の拙いプログラムと考えを晒して、逆に皆様から指摘並びにご教授をいただこう
という考えの下、記事を執筆させて頂いています。大変図々しい。

#リップシンクとは
リップシンクとは、唇の動きと録音された音声を連動させることを指します。
わかりやすく言えば「口パク」です。
今回のプログラムでは、音声の再生を行なっていないため
正確にはリップシンクとは異なりますが、気にしてはいけません。
カタカナ語ってつい使いたくなりますよね。

#環境
今回、プログラムはXcodeで書きました。
パソコンはmacOS Sierraのバージョン10.12.5を使用しています。
また、OpenGLは執筆時点での最新のものを使用しています。
なお、今回はプログラミング内での3次元空間の環境設定や
コールバック関数などの設定については説明を省かせていただきます。ご了承ください。

#唇モデルを描画する
まずは、リップシンクをさせる唇を描画していきます。
人の唇は、大まかに5つのパーツに分けることができます。
スクリーンショット 2018-12-05 1.28.48.png
それぞれ上唇を3パーツ、下唇を2パーツに分割して描画しています。
(画像及びプログラム中では色で分けていますが以降は赤で統一していきます)

lip.cpp
//グローバル変数
double px=0,py=0.5,pz=0.5;//唇上部座標
double px2=-5,py2=0,pz2=-1;//唇左部座標
double px3=5,py3=0,pz3=-1;//唇右部座標
double px4=0,py4=0.5,pz4=0.5;//唇下部座標
------------------------------------------
//描画部分
glBegin(GL_QUADS);//赤
    glVertex3d(px-2,py+1.5,pz+0.5);
    glVertex3d(px+2,py+1.5,pz+0.5);
    glVertex3d(px+1,py,pz);
    glVertex3d(px-1,py,pz);
glEnd();

glBegin(GL_TRIANGLES);//緑
    glVertex3d(px-2,py+1.5,pz+0.5);
    glVertex3d(px-1,py,pz);
    glVertex3d(px2,py2,pz2);
glEnd();
    
glBegin(GL_TRIANGLES);//青
    glVertex3d(px+2,py+1.5,pz+0.5);
    glVertex3d(px+1,py,pz);
    glVertex3d(px3,py3,pz3);
glEnd();
    
glBegin(GL_QUADS);//黄
    glVertex3d(px4,py4,pz4);
    glVertex3d(px2,py2,pz2);
    glVertex3d(px2+2,py2-1,pz2+0.5);
    glVertex3d(px4,py4-2,pz+0.5);
glEnd();
    
glBegin(GL_QUADS);//水
    glVertex3d(px4,py4,pz4);
    glVertex3d(px3,py3,pz3);
    glVertex3d(px3-2,py3-1,pz3+0.5);
    glVertex3d(px4,py4-2,pz4+0.5);
glEnd();

描画の際にpx,py,pzなどの変数を基準に座標を決定していますが、
この中でもpy,px2,px3,py4の4つが唇を動かしていく上でいじる数値となります。
(下記画像中の黒点がそれぞれ該当しています)
スクリーンショット 2018-12-05 1.28.48のコピー.png

#発音時の唇の形状を作る
続いて、先ほど設定した変数を使って唇の座標を調整し、口の形を作っていきます。
pyの値を増加、py4の値を減少させることで唇を縦方向に開くことができます。
また、px2の値を減少、px3の値を増加させれば、唇は横方向に開きます。
更に、px2の値を増加、px3の値を増加させると、唇をすぼめたような形にすることも可能です。
スクリーンショット 2018-12-05 8.29.35.png
これを利用して、5つの母音とnの発音時の唇の形状を再現していきます。

  • a py=3, px2=-5, px3=5, py4=-1.5
  • i py=2, px2=-6, px3=6, py4=--0.5
  • u py=2, px2=-2.5, px3=2.5, py4=-1.0
  • e py=2, px2=-5.5, px3=5.5, py4=-1.5
  • o py=3, px2=-4, px3=4, py4=-1.5
  • n py=0.5, px2=-5, px3=5, py4=0.5
スクリーンショット 2018-12-05 8.40.20.png

静止画で見るとあまり似てるように見えませんが、実際そんなに似てません。
動かせば雰囲気でごまかせるのでこのまま行きます。勢い大事。

#唇に動きをつける準備を行う
ここまで唇の形状を作りましたが、今回行いたいのはリップシンク。
唇が動かなければ意味がありません。
そこで、まずは唇を動かすために、キーボードコールバッグ関数を用いたフラグ管理を行なっていきます。

lip.cpp
//グローバル変数
int si=0;//動作フラグ
double lips[30]={};//リップシンク配列(30音まで)
int l_num=0;//リップシンク配列保存用
int l_num2=0;//リップシンク配列呼び出し用
------------------------------------------
//キーボードコールバッグ関数
void keyboard(unsigned char key, int x, int y)
{

    switch (key) {
        case 27://ESC
            exit(0);
            break;
            
        case '1'://動作開始
            if(si==0){
                si=1;
            }
            break;
   
        case 'a':
            if(si==0&&l_num<30){
                lips[l_num]=1;
                l_num++;
            }
            break;
            
        case 'i':
            if(si==0&&l_num<30){
                lips[l_num]=2;
                l_num++;
            }
            break;
            
        case 'u':
            if(si==0&&l_num<30){
                lips[l_num]=3;
                l_num++;
            }
            break;
            
        case 'e':
            if(si==0&&l_num<30){
                lips[l_num]=4;
                l_num++;
            }
            break;
            
        case 'o':
            if(si==0&&l_num<30){
                lips[l_num]=5;
                l_num++;
            }
            break;
            
        case 'n':
            if(si==0&&l_num<30){
                lips[l_num]=6;
                l_num++;
            }
            break;
            
        default:
            break;
    }
}

上記のlips[]に音を保存し、連続で30音の動作を行えるようにしました。
また、動作中は新規の音を保存できないようにしています。
#唇に動きをつけてリップシンクさせる
先ほど、キーボードコールバック関数で音を保存しました。
あとはディスプレイコールバック関数内で唇を動かすだけです。

lip.cpp
//ディスプレイコールバッグ関数内
if(si==1){
        if(lips[l_num2]==1){//a
            py=3.0;
            px2=-5;
            px3=5;
            py4=-1.5;
            l_num2++;
        }
        if(lips[l_num2]==2){//i
            py=2;
            px2=-6.0;
            px3=6.0;
            py4=-0.5;
            l_num2++;
        }
        if(lips[l_num2]==3){//u
            py=2;
            px2=-2.5;
            px3=2.5;
            py4=-1.0;
            l_num2++;
        }
        if(lips[l_num2]==4){//e
            py=2;
            px2=-5.5;
            px3=5.5;
            py4=-1.0;
            l_num2++;
        }
        if(lips[l_num2]==5){//o
            py=3.0;
            px2=-4.0;
            px3=4.0;
            py4=-1.5;
            l_num2++;
        }
        if(lips[l_num2]==6){//n
            py=0.5;
            px2=-5;
            px3=5;
            py4=0.5;
            l_num2++;
        }
        if(l_num2==l_num){//終了
            py=0.5;
            px2=-5;
            px3=5;
            py4=0.5;
            l_num=0;
            l_num2=0;
            si=0;
            for(int i=0;i<30;i++){
               lips[i]=0;
            }
        }
    }
-----------------------------------------
// 描画部分

これでおそらく動くことでしょう。
早速動かしてみます。
a,i,u,e,o,n,a,oと入力して・・・動作開始!

ezgif.com-video-to-gif.gif
見えませんでした。アッ(終了)

これは、pyなどの数値を直接変更したため、
次の音の口の形状に移行するまでの過程が一切描画されてない事と
一つ一つの音の動きが表示されている時間が1フレームのみになっているため
まともに見ることができない状態になっていた事の2つが原因です。

今度は、口の形状の移行の過程を描画できるようにゆっくり数値を変化させつつ、
音一つ一つの表示時間を長くできるよう書いてみます。

lip.cpp
#define SPEED 3//リップシンク速度
#define TIME 13//リップシンク移行時間

//グローバル変数
int move=0;//経過時間
-----------------------------------------
//ディスプレイコールバック関数内
 if(si==1){
        if(lips[l_num2]==1){//a
            py+=(3.0-py)/SPEED;
            px2+=(-5-px2)/SPEED;
            px3+=(5-px3)/SPEED;
            py4+=(-1.5-py4)/SPEED;
            move++;
            if(move==TIME){
                l_num2++;
                move=0;
            }
        }
        if(lips[l_num2]==2){//i
            py+=(2-py)/SPEED;
            px2+=(-6.0-px2)/SPEED;
            px3+=(6.0-px3)/SPEED;
            py4+=(-0.5-py4)/SPEED;
            move++;
            if(move==TIME){
                l_num2++;
                move=0;
            }
        }
        if(lips[l_num2]==3){//u
            py+=(2-py)/SPEED;
            px2+=(-2.5-px2)/SPEED;
            px3+=(2.5-px3)/SPEED;
            py4+=(-1.0-py4)/SPEED;
            move++;
            if(move==TIME){
                l_num2++;
                move=0;
            }
        }
        if(lips[l_num2]==4){//e
            py+=(2-py)/SPEED;
            px2+=(-5.5-px2)/SPEED;
            px3+=(5.5-px3)/SPEED;
            py4+=(-1.0-py4)/SPEED;
            move++;
            if(move==TIME){
                l_num2++;
                move=0;
            }
        }
        if(lips[l_num2]==5){//o
            py+=(3.0-py)/SPEED;
            px2+=(-4.0-px2)/SPEED;
            px3+=(4.0-px3)/SPEED;
            py4+=(-1.5-py4)/SPEED;
            move++;
            if(move==TIME){
                l_num2++;
                move=0;
            }
        }
        if(lips[l_num2]==6){//n
            py+=(0.5-py)/SPEED;
            px2+=(-5-px2)/SPEED;
            px3+=(5-px3)/SPEED;
            py4+=(0.5-py4)/SPEED;
            move++;
            if(move==TIME){
                l_num2++;
                move=0;
            }
        }
        if(l_num2==l_num){//終了
            py+=(0.5-py)/SPEED;
            px2+=(-5-px2)/SPEED;
            px3+=(5-px3)/SPEED;
            py4+=(0.5-py4)/SPEED;
            move++;
            if(move==TIME){
                l_num=0;
                l_num2=0;
                move=0;
                si=0;
                for(int i=0;i<30;i++){
                    lips[i]=9;
                }
            }
        }
    }

pyなどの数値を直接変えず、変えたい値までゆっくりと加算(減算)していくようにしました。
また、それぞれの音に表示時間を与えて長く動作するようにしました。

もう一度動かしてみます。
ezgif.com-video-to-gifのコピー.gif
今度はしっかり動いてくれました。
なお、本来なら人間はMa,Ba,Paの子音を持つ音を発声する際、
必ず一度口を閉じますが、今回それに対する配慮は特にございません。
一応、nの表示時間のみ短くして各母音の前に置くことで、
擬似的に再現できないこともありませんが・・・要改良です。
また、今回はpy,px2,px3,py4という4つの変数しか動かしていませんが
そのほかの変数も用いれば、よりリアルな動きを再現できます。

#最後に
ここまで読んでいただきありがとうございます。
今回、初めてこのような記事を書かせていただきました。
説明がわかりにくい部分、文章が理解し難い部分など、問題点は多々あると思いますが、
少しでも私の拙いプログラムが、この記事を読んでいただいた皆様の新発想の素になれればと思っています。

それでは、ここで終わりとさせていただきます。
最後まで読んでいただき、ありがとうございました。

#おまけ
今回作成したプログラムを載せておきました。
自由にコピーして煮るなり焼くなりしてください。
lip.cpp
(Githubの使い方が分からなくて限定共有投稿に載せたとは言えない)

2
0
1

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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?