技術

blenderの座標系をOpenGLの座標系に変換する

blenderの座標系は右方向が+x、上方向が+z、奥方向が+yというちょっと特殊な座標系になってます。
対して、OpenGLの座標系はいわゆる右手座標系で、右方向が+x、上方向が+y、奥方向が-zとなってます。

変換するのは簡単で、単にx軸を中心に90°回転するだけでOpenGLの座標系になります。
(x軸の先っぽ?をネジの先端だとすると、ネジを締める方向(時計回り)に90°回すイメージ。回転させるだけで済むということは特殊とは言えblenderの座標系も右手系の一種と言えます。)

blenderでモデリングしたものをOpenGLを使ったプログラムで描画する事は以前からやってて、主にblender → Collada形式でエクスポート → PVRToolsのCollada2POD → iOSのOpenGL ESで利用という流れで行ってました。
この流れの中のどの段階で座標を変換してたといいますと、上記の流れの最後、最終的なプログラムでモデルデータを読み込むときにやってました。

単にモデルを読み込んで表示するだけなら、頂点の座標がどうなってるかなんて考慮する必要はほとんどないのですが、ボーンを仕込んだモデルを扱うようになると、ボーンの座標や回転に気を配る必要があります。

モデル読み込みライブラリの仕様によっては、ボーンに親子関係がある場合などに、90°回転する変換の”掛け方”を工夫する必要があったりします。
例えば、単に各ボーンの姿勢に変換を掛ければいいのか、それともルートのボーンだけに掛けるのか等々。

なんてことを考えると最初から座標系を変換する必要がないほうが分かりやすいのは確かです。

プログラムで読み込む前に座標を変換するには、自分の場合だとblenderの段階、Colladaファイルの段階、Collada2PODの段階のどこかでやる必要があります。

・Collada2PODは右手座標系で出力するか左手座標系で出力するか(z軸を反転するかしないか)のオプションはあるようですが、任意の軸を反転させたり入れ替えたりは出来ないみたいなので却下。

・blenderはPythonスクリプトで拡張できるみたいなので色々やりようはあると思うけど、blenderの内部どころか表面的な部分についても詳しくないのでハマリそう。出来れば最終手段にしたい。
(もっと簡単に、エクスポート前に手動で90°回転させる方法や、そもそも90°回転させた状態でモデリングする方法などありますが、ダサすぎて問題外です)

・Colladaファイルの仕様についてもよく知らないけど、一つのファイルに座標関係のデータがまとまってるし、XMLだから解析しやすそう。

ということで、Colladaファイルの座標を書き換えるプログラムを考えてみたところ、今回自分が使ってるモデルについてはうまく変換してくれるものが出来ました。
1番目の引数に.daeファイルを渡すと、同じディレクトリに変換後の.gl.daeファイルを書き出します。
書き殴り的なものなのでエラー処理もしてなければソースも汚いですし、blenderやColladaやCollada2PODやpodファイルの仕様を調べて作ったわけではないのでロバストさも無いと思いますが、公開します。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

'''
Colladaファイル中のY軸とZ軸を入れ替えて
さらにZ軸を反転する。
blender → OpenGL用
'''

import sys
import numpy
from BeautifulSoup import BeautifulStoneSoup
import re
import os

# ボーンの行列とモデルの頂点と法線を変換するための行列
# (blenderの座標系をx軸を中心に90度回転させるとOpenGLの座標系になる)
conv = numpy.matrix((
    (1.0, 0.0, 0.0, 0.0),
    (0.0, 0.0, 1.0, 0.0),
    (0.0,-1.0, 0.0, 0.0),
    (0.0, 0.0, 0.0, 1.0),
))

file_path = sys.argv[1]

# nodeタグは入れ子を許可する。
# そうしないとBeautifulSoupが勝手にタグを閉じてしまい
# エレメントの親子関係がおかしくなる。
BeautifulStoneSoup.NESTABLE_TAGS['node'] = []

# selfClosingTagsで空タグを許可するタグのリストを渡している
# そうしないと例えば<tag/>を勝手に<tag></tag>に変換してしまう
# 変換されるだけならまだしもエレメントの親子関係がおかしくなる場合があるので、
# 勝手に変換しないように明示的に指定する。
# さらに、Collada2PODはどうも<tag/>という形式が<tag></tag>という形式になってると
# 正常に処理してくれないっぽい。
soup = BeautifulStoneSoup(open(file_path, 'rb'), selfClosingTags=['library_images', 'unit', 'param', 'input', 'instance_camera', 'instance_light', 'instance_visual_scene'])

# ボーンの行列を変換する
for skin in soup.findAll('skin'):
    # まずボーンの行列が全部ひとつにまとまってる部分を変換する
    matrices_element = skin.find(
        'float_array', {'id': re.compile('^.+poses-array$')}
    )
    floats = [
        float(f) for f in matrices_element.string.split(' ')
    ]
    converted = []
    for i in xrange(len(floats) / 16):
        offset = 16 * i
        m = numpy.matrix((
            floats[offset:offset+4],
            floats[offset+4:offset+8],
            floats[offset+8:offset+12],
            floats[offset+12:offset+16],
        ))
        m = m * conv
        for f in m.A1:
            converted.append(f)
    matrices_element.contents[0].replaceWith(u' '.join(str(x) for x in converted))
    
    # 個別に分かてるボーンの行列を変換する
    # 個別に分かてるボーンの行列は相対的なものなので
    # トップレベルの行列のみ変換する
    node = soup.find('node', {'id': 'Armature'}).find('node', recursive = False)
    matrix_elem = node.find('matrix', recursive = False)
    floats = [float(f) for f in matrix_elem.text.split(' ')]
    converted = []
    m = numpy.matrix((
        floats[0:4],
        floats[4:8],
        floats[8:12],
        floats[12:16],
    ))
    m = m * conv
    for f in m.A1:
        converted.append(f)
    matrix_elem.contents[0].replaceWith(
        u' '.join(str(x) for x in converted)
    )

# 頂点と法線を変換する
for vertex_tag in soup.findAll('float_array', {'id': re.compile('^.+positions-array$|^.+normals-array$')}):
    floats = [
        float(f) for f in vertex_tag.string.split(' ')
    ]
    for i in xrange(len(floats) / 3):
        offset = 3 * i
        vertex = numpy.matrix(floats[offset:offset+3]+[1.0])
        vertex =  vertex * conv
        floats[offset] = vertex.A1[0]
        floats[offset+1] = vertex.A1[1]
        floats[offset+2] = vertex.A1[2]
    vertex_tag.contents[0].replaceWith(
        u' '.join(str(x) for x in floats)
    )

# ファイルに出力
file_name, ext = os.path.splitext(file_path)
out_file_path = '%s.gl%s' % (file_name, ext)
with open(out_file_path, 'wb') as f:
    data = str(soup)
    data = data.replace(' />', '/>')
    f.write(data)

PythonでColladaというとpycolladaというパッケージがあり、最初はそれを使ってつくりはじめたのですが、pycolladaだとColladaファイルを読み込んで何も変更せずに保存するだけで、失われるデータ(XMLエレメント)が結構ある事に気付いてやめました。
失われるのは不必要なデータで何の影響もないのかもしれませんが、万一それが原因でハマると修正が面倒そうなので、もっと直接的にXMLをいじるためにBeautifulSoupを使ってます。
まぁBeautifulSoupのほうも、タグを色々勝手に変換してくれるおかげで少々ハマりました。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください