頭と尻尾はくれてやる!

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


SceneKitのHitTestにオプション付けたらなぜかうまくいった

macOS用アプリでSceneKit使ってエディターっぽいものを作っているんだけど、オブジェクトのHit Testがたまにうまくいかない場合があってなんだこりゃ?と悩んでいたんだけど、
{
    NSDictionary *option = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:SCNHitTestBoundingBoxOnlyKey];
    NSArray *results = [scnView hitTest:point options:option];
}
てな感じでbounding box only のオプションをセットすると意図通りに動いた!
ということはややこしい形状のオブジェクトだとHit Testの精度がイマイチなのだろうか?

カメラオブジェクト

↑ややこしいと言ってもこの程度なんだけどなあ?


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軸周りに回転する。あー、すっきりした。


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

最近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オブジェクトは意図通りに動いてくれた。ふー。


macOSでwindowを閉じる時に「保存しないの?」って聞いて欲しい

masOSのDocument-Based ApplicationでwindowをメニューのCloseや⌘+wで閉じる時に保存してない場合、 「保存してないけどええのん?」って聞いてくれる例のあれを実装するのに苦労した話。

そもそもNSDocumentオブジェクトはどうやって保存すべき内容に変更があるかどうか知るんだ?というのも疑問だよな。調べるとNSUndoManagerを使ってundo/redoを実装するなら特に気にせんでいい、実装しないなら
updateChangeCount:
メソッドを実装すりゃええ、ということらしい。

ところがどちらにせよ、いくら頑張っても「保存してないけどええんか?ホンマにええんか?」と聞いてくれない。NSDocumentのdocumentEdited(ダーティフラグっていうのかな?)を見るとちゃんと立ってるはずなんだが、、、?いけずな奴だ。

で、ようやく気付いたんだが、、、
XcodeでDoc-Based Appを新規作成した場合デフォでDocument(NSDocumentのサブクラス)が作成されて、その中に
+(BOOL)autosavesInPlace
ってクラスメソッドがある。デフォでYESを返してるところをNOにしてやると

保存するか聞く画面

↑ちゃんと聞いてくれた!ああ、そういうことかよ、、、orz


なお、NSUndoManagerを使う時、
undoManager = document.undoManager;
のようにNSDocumentオブジェクトからもらいましょう。自分で
undoManager = [UndoManager new];
このように自作してて地味にはまってました。


↓参考サイト、多謝!
NSUndoManagerを使って簡単にアンドゥ、リドゥを実装する




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

FC2Ad