TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

Free-Form Deformationでテクスチャを変形する

前回までの日記で、OpenGLでテクスチャ(2D画像)を平面に描画する方法と、ベジエ曲面を描画する方法について記述した。
この2つを組み合わせて、テクスチャの変形を行う。

テクスチャの変形には、自由形状変形(FFD: Free-Form Deformation)という手法を使う。
FFDは、ベジエ曲面を用いて座標系を変形する。

FFDによる座標系(u, v)の変形は、制御点を\mathbf{P_{ij}}とすると、以下の式で表される。
\displaystyle
\begin{eqnarray*}
\mathbf{X}(u, v) &=& \sum_{i=0}^{m} \sum_{j=0}^{n} B_i^m(u) B_j^n(v) \mathbf{P_{ij}},\;\;\; 0 \leq u \leq 1, 0 \leq v \leq 1 \\
B_i^n(t) &=& {}_nC_i t^i(1 - t)^{n-i}
\end{eqnarray*} \tag{1}
これは、前回の日記で説明したベジエ曲面の式と同じである。

FFDを使用してテクスチャを変形するには、テクスチャを格子状の頂点で構成される面に貼り付けた後、頂点をFFDで変形する。
なお、テクスチャを貼り付ける面は格子状でなくてもよい。この記事では簡単なため格子状とした。

Live2Dの曲面デフォーマーは、このFFDを使用していると思われる。
これによりLive2Dと同じような変形が一部実現できる。

Pythonで記述すると以下のようになる。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image
import numpy as np
import scipy.misc as scm

def bernstein(n, i, t):
    return scm.comb(n, i) * t**i * (1 - t)**(n-i)

def bezier_patches(m, n, u, v, q):
    return np.dot([bernstein(n, j, v) for j in range(n + 1)], np.tensordot([bernstein(m, i, u) for i in range(m + 1)], q, axes=1))

def load_texture():
    img = Image.open("sample2.png")
    w, h = img.size
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tobytes())

def display():
    #P = np.array([
    #    [[ 0. , 0. ], [ 0. , 1/3], [ 0. , 2/3], [ 0. , 1. ]],
    #    [[ 1/3, 0. ], [ 1/3, 1/3], [ 1/3, 2/3], [ 1/3, 1. ]],
    #    [[ 2/3, 0. ], [ 2/3, 1/3], [ 2/3, 2/3], [ 2/3, 1. ]],
    #    [[ 1. , 0. ], [ 1. , 1/3], [ 1. , 2/3], [ 1. , 1. ]],
    #    ])
    P = np.array([
        [[ 0. , 0. ], [ 0. , 0.3], [ 0. , 0.6], [ 0. , 1. ]],
        [[ 0.3, 0.1], [ 0.3, 0.4], [ 0.3, 0.8], [ 0.3, 1.1]],
        [[ 0.6, 0.1], [ 0.7, 0.4], [ 0.7, 0.7], [ 0.6, 1. ]],
        [[ 1. , 0. ], [ 1.1, 0.3], [ 1.1, 0.6], [ 1. , 1. ]],
        ])

    X = np.zeros((11 * 11, 2))
    T = np.zeros((11 * 11, 2)) # texture
    for i in range(11):
        for j in range(11):
            u = i / 10.0
            v = j / 10.0
            X[i * 11 + j] = bezier_patches(3, 3, u, v, P) # FFD

            T[i * 11 + j] = [i / 10.0, 1.0 - j / 10.0]

    V = np.zeros((11 * 11, 3))
    for i in range(11):
        for j in range(11):
            V[i * 11 + j] = [X[i * 11 + j, 0] - 0.5, X[i * 11 + j, 1] - 0.5, 0]

    IDX = np.zeros((10, 11 * 2))
    for i in range(10):
        for j in range(11):
            IDX[i, j * 2] = i * 11 + j
            IDX[i, j * 2 + 1] = (i + 1) * 11 + j

    glClear(GL_COLOR_BUFFER_BIT)

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

    glEnableClientState(GL_VERTEX_ARRAY)
    glEnableClientState(GL_TEXTURE_COORD_ARRAY)

    glEnable(GL_TEXTURE_2D)

    glVertexPointerf(V)
    glTexCoordPointerf(T)

    for idx in IDX:
        glDrawElementsui(GL_TRIANGLE_STRIP, idx)

    glDisable(GL_TEXTURE_2D)

    glDisable(GL_BLEND)

    glColor3f(1.0, 1.0, 1.0)
    glPointSize(2.0)
    glBegin(GL_POINTS)
    for p in P.reshape((-1, 2)):
        glVertex2f(p[0] - 0.5, p[1] - 0.5)
    glEnd()

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"FFDSample1")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    load_texture()
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    glutMainLoop()

if __name__ == '__main__':
    main()

1.0×1.0の大きさの面を10分割してテクスチャを貼り付けている。
4×4の制御点を等間隔の格子から少しずらしている。
その制御点を使用して、FFDで、テクスチャを貼り付けた頂点を変形している。
テクスチャを貼り付けた面の中心が(0, 0)になるように(-0.5, -0.5)だけ移動して表示している。

実行結果
  • 変形前

f:id:TadaoYamaoka:20170309225604p:plain

  • 変形後

f:id:TadaoYamaoka:20170309225659p:plain

白い点は制御点を示している。

参考文献