OBJファイルにPythonでテクスチャ貼りたいですか?

 

{{DZ_TITLE}}
カラーの3D データ作っていますか?
私は時々カラーの3Dデータを作ってたりします。
最近ではBlender等の無料の3Dツールだったり、カラー 3Dプリンターも続々登場していますね。
ちょっと頑張ってお金を出せばカラー 3Dプリンターでのプリントも個人でも依頼するのが夢でなくなってきています。
どうです?それだけでもカラーの3Dデータ作ってみたいと思いませんか?

ただ、実際にはテクスチャはったりするのってUV展開という方法を覚えたりしても中々うまくいかないですね。
人によってはプログラムでテクスチャはったりする方が簡単な人も多いのではないでしょうか?

今回は、そんな人向けにPythonでOBJデータへテクスチャの貼り方を紹介します。

コンセプト

bunny.PNG

具体的には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 2020-05-11-004.png
今見ると、凄く稚拙なCGで恥ずかしいのですが…。

ではコードの記述に行きます!

当然の様におまじないからスタートです。

import os
import numpy as np
import shutil

この段階ではフォルダの中身はこんな状況です。
2020-05-11-001.png

ファイル名操作と、ファイルの準備

ここから、本格的なコードに入ります。
まず、手持ちのデータは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が作成されています。 2020-05-11-002.png

outフォルダを開くとテクスチャ ファイルがリネームコピーされています。
2020-05-11-003.png

簡単な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

今回のプログラムでは…

  1. 点vertexのデータをもとに
    テクスチャvertex textureのデータを作成します。

  2. 面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)

テクスチャのマッピングデータ作成

ここはもう、データを淡々と処理するだけですね。
考え方はこんな感じです。
2020-05-11-005.png

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)

いかがだったでしょうか?
これを使えばこんな感じのファイルが簡単に作れます。 2020-05-11-006.png

いろんな方向から見ると、変な感じになってしまいます。
応用次第ではもっと面白いものができるはずです。
是非チャレンジしてみてください。
2020-05-11-007.png

おすすめ記事

エラーを解消したい ModuleNotFoundError: No module named ‘openpyxl’ - Python
エラーを解消したい ModuleNotFoundError: No module named ‘openpyxl’ - Python
サーバー以外の端末から接続する方法(開発環境版向け、本番環境以外向け) - Django
サーバー以外の端末から接続する方法(開発環境版向け、本番環境以外向け) - Django
全国の町丁目レベル(189,540件)の住所データのオープンデータをPython Pandasで処理してみた
全国の町丁目レベル(189,540件)の住所データのオープンデータをPython Pandasで処理してみた
Django エラー『TemplateDoesNotExist』対策
Django エラー『TemplateDoesNotExist』対策
文字列による条件抽出 - Python Pandas
文字列による条件抽出 - Python Pandas
OpenCV - Python徹底解説
OpenCV - Python徹底解説
Supponsered

PythonをPandasを学びたいなら

Python - Pandas徹底解説

外部サイト
↓プログラムを学んでみたい場合、学習コースなどもおすすめです!

Comments

comments powered by Disqus