2017年 8月

 GLTFSceneKit: SceneKit用glTFローダ / magicien 

image作った。ソースとサンプルアプリはGithubにあります。

未完成な部分もあるけど、基本的な機能は大体出来たかなぁというところ。
SceneKitデフォルトのライトだと、isDoubleSided が機能しない問題があって、どうしようかと考え中。
現状 macOS版だけなので、iOS等にも対応するのと、technique拡張に対応するのが次の課題。
JSceneKitに移植するのがその次の課題。

実際にglTFのパーサを書いてみて感じた利点・欠点はこんな感じ。

利点

  • ロイヤリティフリー
  • JSON形式なので構文解析は自前で作らなくて済む
  • パラメータがOpenGL/WebGLと同じ定数を使っているので、値の変換をせずにそのまま使える(WebGLで使うなら)
  • 上手くやればバイナリデータをそのままGPUに転送できる(WebGLで使うなら)
  • 物理ベースレンダリングに対応している
  • 拡張性が考慮されている

欠点

  • まだ仕様が定まってなさそうな箇所が多い(特に拡張仕様)
  • データ間の関係性が曖昧(一つのデータを違う用途で使い回せたりしてパースし辛いとか、weightsとprimitives/targetsの対応付けとか)
  • WebGL以外で対応しようとするとちょっと大変(特にユーザ定義のシェーダ対応)
  • モーフィングのアニメーションデータが膨大になる(キーフレーム毎に全モーフィングのウェイトを記録する仕様なので、MMDみたいに表情がたくさんあるモデルだと、データ量が増える)
  • MMDのようにモデルとモーションの組み合わせを変えるのが困難(MMDが特殊なのかも)
  • IK・物理演算には対応していない
あとこれ → "Positive rotation is counterclockwise."
OpenGLは回転方向右ねじだったのに逆になったのかな?と思って逆回転で実装したけど、やっぱり右ねじじゃないか!
どっちの方向から見た時に反時計回りなのか書いておくれよ!案の定サンプルモデルのGIF画像が仕様と逆回転しちゃってるでしょうが!

仕様は未完成っぽいけど、今後使いやすくなっていきそう。SceneKitはそのうちglTFを正式サポートするかも?

2017/08/27(Sun) 04:59:04

 SceneKit でテクスチャが赤くなる問題 / magicien 

imageSceneKitでモデルに適用したテクスチャが赤くなる問題があった。

問題が発生するのは、indexed-colorのPNGファイルの一部。原因はよく分からない。
青と緑の成分が無視されている上に、本来sRGBなのにリニアRGBとして処理されているようだ。
モデル自体を変更できるなら、画像ファイルを別のフォーマットに変換すれば良いけれど、できることならプログラム側で対処したい。

で、色々試した結果、NSImageで読み込むと赤くなるが、CGImageで読み込むと大丈夫だということに気づいた。何が何やら...

追記:iOSで試したらUIImageでもCGImageでも色が赤くなってしまった...BugReport送ってみたけど対応してもらえるかなぁ。

2017/08/23(Wed) 07:56:58

 Swift4 で JSON のパース / magicien 

Swift4からCodable、JSONDecoderが使えるようになって、JSONが簡単にパースできるようになった。
とはいえ、すんなり実装できなかった箇所もあったので、試した結果をいくつか記録しておこう。Gistを埋め込む練習ともいう。

基本的な使い方

struct に Codable を付けてあげるだけで勝手にパースしてくれる。structではなく、classでも同じ。
構造体とJSONのデータが食い違っている場合は、DecodingErrorが返ってくる。

必須でない項目、入れ子

Optional型にしておくと、対象のデータが無くてもエラーにならない。 配列はそのまま配列として定義すれば良い。object型の場合は、Codableな構造体かクラスを使う。

キー名を変える

JSONでは user_name だけど、Swiftでは userName という名前を使いたい、という場合、enum CodingKeys を設定する。
CodingKeysを書いた場合、caseに書いてあるものだけパースされる。

自分でパースする

大分大変だけど、init(for decoder: Decoder) を使って自分でパースもできる。
初期化時に何か計算とかしたい場合に。

デフォルト値を指定する

ここから、ちょっとハマったところ。
やりたかったことは次のとおり。
  • 値が存在しなかった場合のデフォルト値を指定したい
  • initは実装したくない
  • JSONとSwiftで同じキー名を使いたい
  • せっかくデフォルト値があるのだから、Optional型にしたくない
  • デフォルト値と同じ値を明示的に設定したかどうか分かるようにしたい
ちなみに、var name: String? = "NoName" としても、パース時に nil で上書きされてしまう。

パースするキーを後から追加する

これもかなり複雑。
Decoderは、CodingUserInfoKey を使ってユーザデータを受け渡しすることができる。
decoder.userInfo[ CodingUserInfoKey(rawValue: "hogehoge")! ] = "UserData" // こんな感じ
やりたかったことは、次のとおり。
  • JSONをパースして何かしらデータを生成するframeworkを作りたい
  • Codableの構造体はユーザには見えないようにしたい
  • ユーザがデコード時にパースするキーとデータ構造を動的に追加できるようにしたい
この例だと、"additionalInfo" の下の "website" の情報を取得している。
ぶっちゃけてしまうと、glTF の extensions/extras のデータをパースしたかった。

パース中のデータのパスを確認する

init(from decoder: Decoder) で、今自分がどのパスにいるのかによって処理を変えたい場合。
glTF の KHR_materials_common の対応に必要だった。ライトとかノードのマテリアルとか、用途も中身も違うものに同じ名前をつけるのやめてよ!

良い例が思いつかないので、実際のglTFのデータの抜粋をパースしてみる。

ジェネリクスとか、Computed Propertiesとか使えば、もうちょっとおしゃれになるかもしれない。

[Float]をSCNVector3・4に変換する

おまけ。encodeも真面目に書いた。
2017/08/22(Tue) 18:20:12

 SceneKitの物理ベースレンダリング(PBR) / magicien 

SceneKitで物理ベースレンダリングを使うには、SCNMaterial.lightingModel = .physicallyBased と設定すれば良い。
つまり、材質によって物理ベースにしたりしなかったりできる。ややこしいね!

physicallyBased の説明によると、
  • 描画に使う SCNMaterial の主なパラメータは、diffuse、roughness、metalness の3つ。
  • 細かい表現には、normal、ambientOcclusion、selfIllumination を使う。
  • ambient、specular、reflective、shininess、fresnelExponent、locksAmbientWithDiffuse は無視される。
  • 他に SCNScene.lightingEnvironment、SCNCamera.wantsHDR を設定すると良い感じになる。
ということらしい。言及すらされない emission さんかわいそう。
ただし、renderingAPI が Metal でないとPBRが有効にならない。GLSLでも実装上問題無いと思うんだけど、シェーダを二種類書くのが面倒だったのかな。

物理ベースでない場合、水面に映り込む背景等は、reflectiveに設定していたけれど、物理ベースの場合は、SCNScene.lightingEnvironment を使うようだ。材質毎に背景を変える理由もないし、こっちの方が良い気がする。
個別に設定したい場合は、selfIllumination を使うと、diffuseの計算に使うテクスチャを変えることができるらしい。
PBRでemissionを使いたい場合、emissionを設定した上で、selfIllumination.content を nil に設定すれば良いようだ。

JSceneKitでシェーダを書くと、上記情報だけでは、フレネル項のパラメータが一つ足りないことに気づく。物理ベースじゃなくてもフレネル反射はしていたので、これまで適当に固定値を割り当てて計算していたけれど、SceneKitではどう計算しているのだろう。
試しにFox2をPBRで描画してみたら、地面が暗すぎてよく見えなくなった。というわけでHDR(トーンマッピング)にも対応せねばなるまい。

2017/08/16(Wed) 04:10:41

 JSceneKitでMMDモデル・モーション表示デモ公開 / magicien 

紹介動画をアップしました。

デモページはこちら。
ニコニ立体ちゃん(アリシア・ソリッド)のモデルを初めて表示したとき、パンツ丸見えじゃん...どこがどうバグったらこんなことになるんだ...と思って、公式サイトを確認しに行ったら、元から丸見えだったよ!せっかく縦回転の角度に制限付けたのに台無しだよ!

2017/08/13(Sun) 09:58:25

 TGA画像表示用ライブラリを作った / magicien 

とあるモデルを表示しようとしたら、テクスチャがtgaファイルのためブラウザでうまく読み込めなかった。
仕方ないのでパーサを作り、ついでにnpmのパッケージとして公開した。

Githubページはこちら
調べてみたらTGAは結構古いフォーマットみたいで、今後ブラウザがサポートするのは望み薄。需要はほぼ無い気がするけど、WebGLでtgaのテクスチャを使いたい人が自分以外にもいるかもしれないので、パッケージ化しておくことにした。

サンプルページにはレナさんの画像を使わせていただいた。Gimpでtgaに変換したものを表示させてみたが、うまくいったようだ。画像処理系はサンプルに困ったらレナさんを使えばいいや、というのがあって気が楽で良い。

3Dでサンプルとして自由に使えそうなのだと、ティーポットとかうさぎとかだろうか。でもMMDみたいにボーンがあって、さらにアニメーションも含めて気兼ねなく使えて、自由に共有できるものがなかなか無い。

2017/08/05(Sat) 13:50:10

 SCNShadable.shaderModifiers の色に要注意 / magicien 

SCNShadable.shaderModifiers でシェーダを書くと、なぜか想定より色が暗くなってしまう。丸一日悩んだ結果、内部で sRGB→RGB の変換がされていることがわかった。
(もしかしたら SceneKitのバージョンによって挙動が違うかもしれないし、macOS/iOS/tvOS/watchOS でそれぞれ挙動が違うことが多々あるので要注意。)

例えば、SCNMaterial の ambient.contents に NSColor(0.5, 0.5, 0.5, 1.0) を設定すると、shaderModifiers の _surface.ambient は float4(0.214, 0.214, 0.214, 1.0) ぐらいの値になる。
逆に、_output.color を float4(0.214, 0.214, 0.214, 1.0) にすると、画面出力は (0.5, 0.5, 0.5, 1.0) になる。
ので、あまり気にしなくても良いかもしれないが、シェーダ内でライトの色と物体の色を掛けたり足したりすると、どんどん値が想定とずれていってしまう…
真面目にやるなら、RGBからsRGBに変換して色を計算、最後にsRGBからRGBに戻すことになる。二度手間だなぁ。
計算式は、Cygames Engineers' Blogを参考に近似式を使ってこんな感じにした。

inline float3 linearToSrgb(float3 c) {
    return pow(c, float3(1.0/2.2));
}

inline float4 linearToSrgb(float4 c) {
    return pow(c, float4(1.0/2.2));
}

inline float3 srgbToLinear(float3 c) {
    return pow(c, float3(2.2));
}

inline float4 srgbToLinear(float4 c) {
    return pow(c, float4(2.2));
}

#pragma arguments

...

#pragma transparent
#pragma body

float4 materialDiffuse = linearToSrgb(_surface.diffuse);
…
float3 lightAmbient = linearToSrgb(_lightingContribution.ambient);
…

_output.color = srgbToLinear(_output.color);
Metalはinline命令使えないと思ったけど使えた。ちゃんとインライン展開されているのかな?
float4の場合はアルファ値を変換しないのが正しい気がする。後でSceneKitの挙動を確認しよう。
SceneKit側は厳密な変換をしているようなので、上の方法だとわずかにずれるけど実用上問題ないと思う。
SCNShadable のページで、自作関数を #pragma arguments の下に書いてある例があるけど、関数は #pragma arguments の前に書かないと、うまく動作しなかった。

2017/08/03(Thu) 00:22:54