頭と尻尾はくれてやる!

パソコンおやじのiPhoneアプリ・サイト作成・運営日記


SceneKitで子ボーンの回転

ここのところ3DエディターっぽいmacOSアプリを作ってて、SceneKitでボーンを自在に回転させようとしたらえらい苦労した話。

Blenderで作成したオブジェクト

↑Blenderで作成したボーンを持つオブジェクト。これをdaeで出力。

daeファイルを表示

↑そのdaeファイルをSceneKitでインポートして表示(画像は作成中のアプリの一部、Xcodeではない)。ここまではええねん。

任意の子ボーンを任意の軸まわりで回転させたいわけだが、、、話を簡単にするためにz軸周りに回転させる、とする。
この「z軸周り」というのは世界座標系でのz軸ね。
一番の親ボーンの回転はできる。問題はその子ボーン、さらにその子ボーン、、、などを意図通り回転させられないことだ。

あるボーンを回転させても、それ以下の子ボーンオブジェクト(SCNNode)の持つ姿勢(orientationプロパティ)を表すクォータニオンは変わらない。
なので、ターゲットの子ボーンオブジェクトを回転させようとすれば、その親ボーンの姿勢を考慮して、さらにその親の、、、と一番の親まで考慮しなければならない、、、?なんか面倒だよなあ?
と思ってたらリファレンスに

- (SCNMatrix4)convertTransform:(SCNMatrix4)transform toNode:(SCNNode *)node;
- (SCNMatrix4)convertTransform:(SCNMatrix4)transform fromNode:(SCNNode *)node;

などのメソッドがあってnodeにnilを使えば世界座標系に変換できるから、ああ、これを使えばいいんだな、と思って取り組むもうまくいかなかった。

結局試行錯誤の結果、、、処理の流れとしては自分の姿勢を世界座標で表してそこでz軸周りの回転を与え、それを元にもどしてやる、という流れだとうまくいった。世界座標に変換するのに上記のメソッドでいけそうなのもんだけど、うまくいかず結局自作(GetQuaternionToWorldForBone)した。
↓コードはこんな感じで。MySceneKitUtilityなどところどころ自作のUtilityメソッドがあるけど、GLKitベースのメソッドをSceneKitで使えるようにしてるだけなのでここでは省略。

{
    SCNQuaternion(^GetQuaternionToWorldForBone)(SCNNode *) = ^(SCNNode *targetNode) {
            //世界座標に変換のためのquaternionを得る(自分自身は含まない)
            
            SCNQuaternion quat = QuaternionIdentity;
            
            while (1) {
                if ([targetNode.name isEqualToString:TOP_BONE_NAME]) {
                    break;
                }

                SCNNode *pNode = [targetNode parentNode];
                quat = [MySceneKitUtility quaternionMultiplyLeft:pNode.orientation right:quat];
                targetNode = pNode;
            }
            return quat;
    };


        SCNQuaternion toWorldQuat = GetQuaternionToWorldForBone(bone);
        SCNQuaternion toLocalQuat = [MySceneKitUtility scnQuaternionInvert:toWorldQuat];
        
        SCNQuaternion localBoneQuat = bone.orientation;
        SCNQuaternion worldBoneQuat = [MySceneKitUtility quaternionMultiplyLeft:toWorldQuat right:localBoneQuat];
        
        SCNQuaternion newWorldBoneQuat = [MySceneKitUtility quaternionMultiplyLeft:dQ right:worldBoneQuat];
        SCNQuaternion newLocalBoneQuat = [MySceneKitUtility quaternionMultiplyLeft:toLocalQuat right:newWorldBoneQuat];

        bone.orientation = newLocalBoneQuat;
}
z軸周りに少し回転させるクォータニオンがdQ。
一番大元のボーンには TOP_BONE_NAME という名前を付けてる。
while(1) なんてやる場合には無限ループにならないようにしましょう(省略してるけど)。

実際に動かすと以下のような感じ。z軸周りに30度回転させてる。

全体を回転

↑一番大元のボーンを対象にすると全体が回転する。

子ボーンを回転(1)

↑途中のボーンを回転。

子ボーンを回転(2)

↑下側のボーンとか。

この方法だとBlenderから持って来たファイルを使う時のY-upとかも気にしなくていい。どちらにせよとにかくz軸周りに回転する。あー、すっきりした。


100均とANKERの車内用バッテリーチャージャー

セリアの車内用バッテリーチャージャー

↑100均(セリア)で車内用のバッテリーチャージャーを買ったんだ(以前使ってたのが壊れたので)。
ところが、これが充電してくれない、、、いや、わずかに増えるから壊れてるわけじゃないんだ。でもアプリとか起動してると少しずつ減って行くほどなんだよ。まあ、100均だから仕方ないか、とあきらめてAmazonで別なのを買ってみた。



↑これを使ってみたら全然違う!アプリ使っててもちゃんと充電されていく!100均のが800mAって書いてて、ANKERのは1ポート最大2.4Aとあるんだけど、この違いなのかな?比較できる値なのかよくわからんのだけど。
ともかく、100均よりはだいぶ高い(といっても1000円もしないのだが)けど期待通りに動いてくれるのでめでたし、めでたし。



オブジェクト選択時の輪郭線を実装したい

最近3DのエディターっぽいものをmacOSで作ってます。SceneKit 、 Objective-C で。

Blenderオブジェクト選択時

↑BlenderやSceneKitのエディターでは3D空間上にあるオブジェクトをクリックするとそのオブジェクトを選択している目印として黄色っぽい色の輪郭線が表示される(画像はBlenderの場合)。

こういうのを表現したいのだけど、、、考えたらこれ相当面倒だよね?
苦肉の策でやったのがクリックされたオブジェクトをコピーしちょっと拡大して、その背面(三角形の裏面やな)を表示する、というトゥーンシェーダーなどで輪郭線を描く場合によく紹介されてる手法。

SCNNodeオブジェクトをコピーしたい
↑オブジェクトのコピーはこんな感じで。コピーしたオブジェクトのボーンを消す必要があることに気付くまで随分時間がかかってしまったけ。

{
    SCNMaterial *firstMaterial = newNode.geometry.firstMaterial;
     firstMaterial.cullMode = SCNCullModeFront;
}
↑materialにお好みの色を設定するけど、それ以外にも三角形の裏面を表示するんやで、とするのにcullModeプロパティ(デフォはSCNCullModeBack)を設定。SCNCullModeFrontが裏面って覚えられへんわ、俺。

オブジェクト非選択時 オブジェクト選択時
↑右がクリックした後、、、まあそれっぽいんだけどさコピーしたオブジェクトのscaleをどうすんのよ?って感じなんだけど、、、カメラとオブジェクトの距離の関数でscaleを決めてもイマイチだったんだなあ(改良の余地はあったかもしれん)。選択されてるのはわかるからもうこれでいいんだけどさ。
Blenderみたいにどんなサイズでも綺麗に1pxの輪郭線を描くのって、、、どうしてるんだろうね。


SCNNodeオブジェクトをコピーしたい

SceneKitのSCNNodeオブジェクトをコピーしたかったんだわ。
copyでいいのかな、、、うまくいかねえ、mutableCopyかな、、、?これもだめか、、、調べるとリファレンスに
- (void)duplicateNode:(SCNNode *)node withMaterial:(SCNMaterial *)material
{
    SCNNode *newNode = [node clone];
    newNode.geometry = [node.geometry copy];
    newNode.geometry.firstMaterial = material;
}
なんてある。↓ここね。
clone - SCNNode | Apple Developer Documentation
geometryやmaterialも参照しちゃうので別に使うなら新たに設定しないとだめってことか。なるほどね。

ところが、これでもうまくいかない。新しいオブジェクトのscaleもpositionも設定しても変化ない、、、つまり元のオブジェクトと変わってない、、、?

同じ位置に表示されてチラチラする

↑こんな感じに別な場所やscaleで描こうとしてるのに同じ場所に同サイズで表示してるので時折黒い面(コピー後のオブジェクトの色)が描画される、というよく見るよね、3D関連やってると。

もう新しく作るものなんてないのでは?と悩むこと幾星霜、ようやく気付いたのがこのコピー元のオブジェクトにはボーンがあって動きはそっち優先になってたのが原因。
{
    newNode.skinner = nil;    
}
として消してやれば新しいSCNNodeオブジェクトは意図通りに動いてくれた。ふー。


TFRecordで多次元配列を扱う

TFRecordでたくさんの浮動小数点数を扱う
numpyで値の型を指定したつもりが
↑このあたりに関連したデータの整形のお話。

TensorFlowで多次元の配列でデータを受け取りたいとする。
まずは保存。上記リンクにあるようにnumpyを使う。3次元の場合はこんな感じでやってみた。値は32bitのfloatで。
{
    CH = 4
    W = 3
    H = 2
    ite = 0
    arr_h = np.zeros([0,W,CH] , dtype=np.float32)
    for h in range(H) :
        arr_w = np.zeros([0,CH] , dtype=np.float32)
        for w in range(W) :
            arr_ch = np.zeros([0] , dtype=np.float32)
            for ch in range(CH) :
                arr_ch = np.append(arr_ch , np.float32(ite)) 
                ite += 1
            arr_w = np.append(arr_w , [arr_ch] , axis=0)
        arr_h = np.append(arr_h , [arr_w] , axis=0)
    print(arr_h)
}
#[[[ 0. 1. 2. 3.]
# [ 4. 5. 6. 7.]
# [ 8. 9. 10. 11.]]
#
# [[ 12. 13. 14. 15.]
# [ 16. 17. 18. 19.]
# [ 20. 21. 22. 23.]]]

↑結果。意図通り2x3x4の3次元配列になってる。append時のキャストが必要なのはリンクの通り。
これをTFRecordに書き込んだとする。
次に、このデータを使う時に読み込む場合。
{
    features = tf.parse_single_example(
                                       serialized_example,
                                       features={
                                       'arr_h': tf.FixedLenFeature([], tf.string),
                                       })
    arr = tf.decode_raw(features['arr_h'], tf.float32)
    arr = tf.reshape(arr, tf.stack([2,3,4]))
}
reshapeが必要。画像サイズがまちまちだと画像ごとにW,H,CHの値を持たせてる場合もあったと思う(確かcifar10とか?)が定数なら持たせなくてもいいか。
TensorFlowのテンソルとして値を表示させてみると、、、

_arr1=[[[[ 0. 1. 2. 3.]
[ 4. 5. 6. 7.]
[ 8. 9. 10. 11.]]

[[ 12. 13. 14. 15.]
[ 16. 17. 18. 19.]
[ 20. 21. 22. 23.]]]]

ふんふん、それらしくなってるな。とまあここまでしか確認はできていなくて、これからこれらを使って実際に学習が進むかってのをやっていくわけだけど。




Copyright ©頭と尻尾はくれてやる!. Powered by FC2 Blog. Template by eriraha.

FC2Ad