OBJファイルにPythonでテクスチャ貼りたいですか?
カラーの3D データ作っていますか?
私は時々カラーの3Dデータを作ってたりします。
最近ではBlender等の無料の3Dツールだったり、カラー 3Dプリンターも続々登場していますね。
ちょっと頑張ってお金を出せばカラー 3Dプリンターでのプリントも個人でも依頼するのが夢でなくなってきています。
どうです?それだけでもカラーの3Dデータ作ってみたいと思いませんか?
ただ、実際にはテクスチャはったりするのってUV展開という方法を覚えたりしても中々うまくいかないですね。
人によってはプログラムでテクスチャはったりする方が簡単な人も多いのではないでしょうか?
今回は、そんな人向けにPythonでOBJデータへテクスチャの貼り方を紹介します。
コンセプト
具体的にはOBJファイルと画像ファイルを読み込んで無理やりマージする感じで行きます。
Python好きな人だったら、Stanford BunnyとPythonが合体する夢を何度も見ていると思います。(嘘です)
結果、Pythoned Stanford Bunnyが完成します。
全てのディスプレイの形状がStanford Bunnyだったらコード書くのつらいですね。
今回はこんなものを作るプログラムを作っていきます。
OBJファイルをどうやって入手するのか?
例えば、Thingivers等で公開されているデータをOBJファイルに変換をする何て事も可能です。
基本的にThingivers等は非カラー3D Printer向けのデータが多いためSTLファイルという形式がほとんどです。
BlenderやMeshLab等のフリーツールでSTLからOBJに変換が可能です。
例えば、私が公開しているバベルの塔などで試してみるのも良いかもしれません。
https://www.thingiverse.com/thing:1741015
今見ると、凄く稚拙なCGで恥ずかしいのですが…。
ではコードの記述に行きます!
当然の様におまじないからスタートです。
import os
import numpy as np
import shutil
この段階ではフォルダの中身はこんな状況です。
ファイル名操作と、ファイルの準備
ここから、本格的なコードに入ります。
まず、手持ちのデータはOBJファイル(3D データ)と、画像ファイルです。
そのファイルをもとに 子フォルダoutを作成します。
そのoutフォルダに、画像をコピーします。
また、新しいOBJファイルと、出力されるmtlファイルの名前をここで定義します。
# 読み込むファイル名
obj_filepath = r'D:\Data\3D\bunny.obj'
img_filepath = r'D:\Data\3D\test.jpg'
# ファイルのサブフォルダ「out」を作成し、出力するファイル名を決定する
base_dir , filename = os.path.split( obj_filepath)
base_name , ext = os.path.splitext(filename)
out_dir = os.path.join( base_dir, 'out')
out_obj_filepath = os.path.join( out_dir, filename)
out_mtl_filename = base_name + '.mtl'
out_mtl_filepath = os.path.join( out_dir, out_mtl_filename)
_ , img_ext = os.path.splitext( img_filepath)
out_img_filename = base_name + img_ext
out_img_filepath = os.path.join( out_dir, out_img_filename)
# outフォルダがない場合作成する
if not os.path.exists(out_dir) :
os.mkdir(out_dir)
# 画像ファイルをコピーします。
shutil.copyfile(img_filepath, out_img_filepath)
# 結果表示
out_dir, out_obj_filepath, out_mtl_filepath, out_mtl_filename, out_img_filepath, out_img_filename
('D:\\Data\\3D\\out',
'D:\\Data\\3D\\out\\bunny.obj',
'D:\\Data\\3D\\out\\bunny.mtl',
'bunny.mtl',
'D:\\Data\\3D\\out\\bunny.jpg',
'bunny.jpg')
ここまで来たらフォルダoutが作成されています。
outフォルダを開くとテクスチャ ファイルがリネームコピーされています。
簡単なOBJ規格解説
実はOBJファイルの読込は凄く簡単です。
何故って、テキストファイルで誰でも簡単に覗くこともできて、仕様も色々な人が公開しています。
また、OBJファイルと画像ファイルだけではテクスチャを貼ることはできず、MTL(マテリアル)ファイルを用意する必要があります。
OBJファイルと、MTLファイルをそれぞれ紹介していきます。
OBJファイルの構造
マテリアルファイルの指定
いきなりマテリアルファイルが来ます。
今回は、独自のマテリアルファイルを作成するので、この行は無視してしまいます。
mtllib bunny.mtl
点の定義
OBJファイルでは、まず点を定義します。
vはvertexの頭文字で点という意味です。
x y zの順番で格納されています。
v -0.037830 0.127940 0.004475
v -0.044779 0.128887 0.001905
v -0.068010 0.151244 0.037195
法線ベクトルです。
面の内側、外側を判定する部分です。
vertex normの略語ですね。
ここは理解する必要は、あまりないでしょう。 (ガチ勢以外)
vn -6.112566 0.913688 -1.122156
vn 0.344287 3.684636 5.052224
vn -1.539003 6.070478 -0.417480
テクスチャの定義
vertex textureの略でわかると思いますが、点とテクスチャの対応を示しています。
画像のサイズがどんなサイズだったとしても、値は0~1です。
vt 0.308518 0.491707
vt 0.308685 0.485981
vt 0.299225 0.880764
面の定義
faceの略で、点をそれぞれつなぐ方法です。
今回は、真ん中のテクスチャvtを修正していきます。
色々な表現方法がります。
順番は v、vt、vnの順番で並んでいます。
今回のサンプルでは元のデータにv、vt、vnがすべてある状態を想定しています。
足りない時点でエラーになります。
独自に完全版にしたい場合、独自に使っていただけたらと思います。
f v1//vn1 v2//vn2 v3//vn3
f 5267//5267 5268//5268 5402//5402
f 5403//5403 5537//5537 5536//5536
f 5402//5402 5403//5403 5536//5536
f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
f 30205/51430/30205 28477/51431/28477 27408/51432/27408
f 9150/51433/9150 9259/51434/9259 9258/51435/9258
f 9761/51436/9761 7565/51437/7565 9672/51438/9672
今回のプログラムでは…
-
点vertexのデータをもとに
テクスチャvertex textureのデータを作成します。 -
面faceの2番目のvertex texture部分を書き換えます。
という流れになります。
MTLマテリアルファイルは
以下の様になります。
map_Kdの欄にテクスチャのファイルを追加します。
newmtl Material
Ns 323.999994
Ka 1.000000 1.000000 1.000000
Kd 0.800000 0.800000 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
map_Kd bunny.png
OBJファイルの読込ソースコード
以下の様になりました。
戻り値が長いですが、ご愛嬌と言う事にしてください。
def read_obj_file(obj_filepath) :
"""
Objファイルを読込
Parameters
----------
filepath : str
読込むObjファイル
Returns
-------
v_lst, vn_lst, f_lst list
点、線、面をListに格納
x_min, y_min , z_min
x, y, z の最小値
x_max, y_max , z_max
x, y, z の最大値
"""
v_lst = []
vn_lst = []
f_lst = []
x_min = y_min = z_min = np.inf
x_max = y_max = z_max = -np.inf
# OBJファイル読込
with open(obj_filepath,'r') as fp:
for line in fp:
ary = line.strip().split(' ')
if ary[0] == 'v' :
dat = [ float(ary[1]) , float(ary[2]) , float(ary[3]) ]
v_lst.append( dat )
x_min = min(x_min,dat[0])
y_min = min(y_min,dat[1])
z_min = min(z_min,dat[2])
x_max = max(x_max,dat[0])
y_max = max(y_max,dat[1])
z_max = max(z_max,dat[2])
elif ary[0] == 'vn' :
vn_lst.append( [ float(ary[1]) , float(ary[2]) , float(ary[3]) ])
elif ary[0] == 'f' :
f_lst.append( [ ary[1] , ary[2] , ary[3] ])
return v_lst , vn_lst , f_lst , \
x_min, y_min, z_min , \
x_max, y_max, z_max
# OBJファイルを読込 を実施
v_lst , vn_lst , f_lst , \
x_min, y_min, z_min , \
x_max, y_max, z_max = \
read_obj_file(obj_filepath)
# 結果表示
v_lst[:3] , vn_lst[:3] , f_lst[:3] , \
x_min, y_min, z_min , \
x_max, y_max, z_max \
([[-0.03783, 0.12794, 0.004475],
[-0.044779, 0.128887, 0.001905],
[-0.06801, 0.151244, 0.037195]],
[[1.22342, 6.106969, -0.789864],
[1.351736, 5.963559, -1.435807],
[0.367206, 5.014973, 3.728925]],
[['21217//21217', '21216//21216', '20400//20400'],
['9187//9187', '9281//9281', '14839//14839'],
['16021//16021', '13434//13434', '5188//5188']],
-0.09469,
0.032987,
-0.061874,
0.061009,
0.187321,
0.0588)
予備の関数用意
arduino触った人はmap関数を知っている人は多いのではないでしょうか?
今回はそれをpythonに移植しました。扱うデータはfloat型もOKな様にしています。
便利なのでPythonにも欲しいですね。 私が知らないだけ?
利用目的はOBJファイルのサイズに対して、画像ファイルをマッピングするためです。
OBJファイルの説明欄で書いた様に、どんなサイズのOBJファイルだったとしても、どんなサイズの画像ファイルだったとしても、vtでは0~1の範囲になります。
def map(x, in_min, in_max, out_min, out_max):
"""
in_min~in_maxの範囲からout_min,out_maxの範囲に変換、変換する値はx
Parameters
----------
x float
変換したい値
in_min float
変換前の最小値
in_max float
変換前の最大値
out_min float
変換後の最小値
out_max float
変換後の最大値
Returns
-------
out_x float
変換後の値
"""
return float((x-in_min) * (out_max-out_min) / (in_max-in_min) + out_min)
テクスチャのマッピングデータ作成
ここはもう、データを淡々と処理するだけですね。
考え方はこんな感じです。
def map_obj_texture( x_min, y_min, z_min , x_max, y_max, z_max ):
"""
Objファイルとイメージのマッピングを行う
Parameters
----------
x_min float
xの最小値
y_min float
yの最小値
z_min float
zの最小値
x_max float
xの最大値
y_max float
yの最大値
z_max float
zの最大値
Returns
-------
new_t_o_lst list
テクスチャをフェイスに張り付けたときの番号
t_lst
テクスチャの位置
"""
new_t_lst = []
new_t_o_lst = []
t_lst = []
for f in f_lst:
new_t_i_lst = []
for i in range(3):
f1 = f[i]
f_ary = f1.split('/')
f1_0 = int(f_ary[0])
v = v_lst[f1_0-1]
pos0_x = map( v[0] , x_min, x_max, 0, 1)
pos0_y = map( v[1] , y_min, y_max, 0, 1)
pos0_z = map( v[2] , z_min, z_max, 0, 1)
t_lst.append([pos0_x,pos0_y])
new_t_i_lst.append(len(t_lst))
new_t_lst.append( [ f_ary[0] , len(t_lst) , f_ary[2] ])
new_t_o_lst.append( new_t_i_lst )
return new_t_o_lst , t_lst
new_t_o_lst , t_lst = map_obj_texture( x_min, y_min, z_min , x_max, y_max, z_max )
# 最初の3つだけ表示
new_t_o_lst[:3] , t_lst[:3]
([[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[0.015260213617300067, 0.643908665621315],
[0.016120848560363252, 0.6438049943628754],
[0.012350753697840026, 0.6350188552101288]])
MTLマテリアルファイルの作成
ここは厳密にいうと細かい指示が必要なのかもしれません。
ただ、今回はとりあえずテクスチャ用の画像を定義するためだけに、このファイルを生成します。
不明な内容が多いですが、無視しておきます。
def save_mtl(out_mtl_filepath, out_img_filename):
with open(out_mtl_filepath,'w+') as fp:
fp.write("newmtl Material\n")
fp.write("Ns 323.999994\n")
fp.write("Ka 1.000000 1.000000 1.000000\n")
fp.write("Kd 0.800000 0.800000 0.800000\n")
fp.write("Ks 0.500000 0.500000 0.500000\n")
fp.write("Ke 0.000000 0.000000 0.000000\n")
fp.write("Ni 1.450000\n")
fp.write("d 1.000000\n")
fp.write("illum 2\n")
fp.write("map_Kd %s\n"%(out_img_filename))
save_mtl(out_mtl_filepath, out_img_filename)
OBJファイル保存
もともとのOBJファイルの情報に、テクスチャをマッピングした情報を足し合わせて保存します。
何も難しい要素はないと思います。
def save_obj_with_texture( out_obj_filepath, out_mtl_filename , v_lst, vn_lst, f_lst, new_t_o_lst, t_lst):
with open(out_obj_filepath , 'w+') as fp:
fp.write('mtllib %s\n'%(out_mtl_filename))
for v in v_lst:
fp.write('v %f %f %f\n'%(v[0],v[1],v[2]))
for vn in vn_lst:
fp.write('vn %f %f %f\n'%(vn[0],vn[1],vn[2]))
for t in t_lst:
fp.write('vt %f %f\n'%(t[0],t[1]))
fp.write('usemtl Material\n')
for idx,f in enumerate(f_lst):
new_t_o1_lst = new_t_o_lst[idx]
res = 'f '
for i in range(3):
f_ary = f[i].split('/')
res += '%s/%d/%s '%(f_ary[0],new_t_o1_lst[i],f_ary[2])
fp.write(res)
fp.write('\n')
save_obj_with_texture( out_obj_filepath, out_mtl_filename, v_lst, vn_lst, f_lst, new_t_o_lst, t_lst)
いかがだったでしょうか?
これを使えばこんな感じのファイルが簡単に作れます。
いろんな方向から見ると、変な感じになってしまいます。
応用次第ではもっと面白いものができるはずです。
是非チャレンジしてみてください。
おすすめ記事
エラーを解消したい ModuleNotFoundError: No module named ‘openpyxl’ - Python
サーバー以外の端末から接続する方法(開発環境版向け、本番環境以外向け) - Django
全国の町丁目レベル(189,540件)の住所データのオープンデータをPython Pandasで処理してみた
Django エラー『TemplateDoesNotExist』対策
文字列による条件抽出 - Python Pandas
OpenCV - Python徹底解説
Supponsered
PythonをPandasを学びたいなら
外部サイト
↓プログラムを学んでみたい場合、学習コースなどもおすすめです!