著者名が「magicien
51〜60件目 / 225件
前へ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 次へ

 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

 JSceneKitでカプセルとConcaveの剛体の衝突判定を実装した / magicien 

image 試してみたい人はサンプルページのExample 2へ。これでFoxの移植でやるべきことは大体終わったと思う。

衝突判定の実装で参考にしたのは、ゲームつくろー!その掲示板
カプセルとConcaveの衝突判定は、結局のところ線分と三角形の距離を計算するということだった。
実は衝突判定のオレンジ色の本を以前買ったのだけれど、実家に置いてきてしまったので泣く泣くネットで資料を探しつつコードを書いた。

壁にぶつかる様子を動画にしてみたけど、前回の動画と見た目があまり変わらないのでアップするのをやめた。
Fox2を移植してみて、それと併せて動画化するのが良いかと思案中。

画像はFox2のステージを試しに表示したもの。Badgerが死ぬほど遅かったので心配だったけれど、ゲームとして遊べる程度の速度は出そう。

2017/07/19(Wed) 05:05:19

 いろいろと更新中 / magicien 

気づいたらサーバのPHPバージョンが5から7になって動かなくなったものが出てきたので修正中。

ついでに左側に3Dライブラリのリンクを追加しました。PHP7になってもほとんどの関数はそのまま使えたので、ほぼ修正なしで大丈夫でした。ついでに使っているフレームワークも最新化。

image JSceneKitの方は、物理演算で疲れたので、Badgerの移植で気分転換中。ただものすごく遅い...FPSが0.1とかいうレベルなので、高速化しないとデバッグもできない。とりあえず、トロッコが線路外を走っているのは分かった。





2017/07/08(Sat) 06:22:58

 scnファイルのデータ構造 / magicien 

JSceneKitを実装する上で難しかった点の一つは、scnファイルの解析である。とはいえ、pmmファイルの解析で鍛えられたバイナリ読解力を以ってすれば造作もない。

試しに ship.scn をバイナリエディタで開いてみると、"bplist00" という文字列が最初に入っていることがわかる。
これは、binary property list形式ということなので、plutilコマンドで中身を見ることができる。
$ plutil -p ship.scn

実行結果はこんな感じ。
{
  "$version" => 100000
  "$objects" => [
    0 => "$null"
(中略)
    81 => {
      "$classname" => "SCNScene"
      "$classes" => [
        0 => "SCNScene"
        1 => "NSObject"
      ]
    }
  ]
  "$archiver" => "NSKeyedArchiver"
  "$top" => {
    "root" => <CFKeyedArchiverUID 0x7f90efd00480 [0x7fffbe406da0]>{value = 1}
  }

JSONのような形式で内容が表示されるので、これを元にどこにどのデータが入っているかを解析する。
すると、ファイルの末尾26バイトにパースに必要なデータが入っていることがわかる。
  • オフセットデータのサイズ:unsigned byte
  • オブジェクトのindexのサイズ:unsigned byte
  • オブジェクト数:unsigned long long
  • 最初のオブジェクトのindex:unsigned long long
  • オブジェクトデータの場所:unsigned long long
次に、オブジェクトデータの場所(ファイル先頭からのバイト数)からオブジェクト数の数だけ、整数(バイト数は、オフセットデータのサイズ)を読み込む。これは、各オブジェクトのデータの場所を表している。

最初に読み込むべきオブジェクトは、「最初のオブジェクトのindex」で与えられている。
各オブジェクトデータの先頭1バイトに、データの種類が格納されている。
0x00 なら null、0x08 なら false、 0x09 なら true、0x10〜0x1F なら整数、等。

最初のオブジェクトは、$version、$objects、$archiver、$top というデータの入ったDictionary型になっている。
次に読み込むべきオブジェクトは、$objects[$top.root] のオブジェクト。このオブジェクトが本当に必要なデータということになる。
オブジェクトが何らかのインスタンスである場合は、Dictionary型になっていて、"$class" にどのクラスのデータかが記録されている。
scnファイルの場合は、$class.$classname が SCNScene となっているはず。
Dictionary内の残りのデータはクラスによって異なるので、どれをどう読み込むかはクラス毎に実装が必要。
インスタンスのデータ全てが出力されているかといえばそういうわけではなく、デフォルト値と同じ場合は出力されていないこともある。

文字で書くと意味不明ですね。OmniGraffleの試用期間が終わったので製品版買おうかと思ったけど、Standardでも思ったより値が張るなぁ。
2017/06/28(Wed) 04:03:27

 SceneKit:主な構成要素、その関連性 / magicien 

を読み進めていたあなたは、SceneKitについて書かれたページを見つけました。

image そのページには、挿絵とともに、次のような文言が記載されていました。
・全体の見た目を変えるなら、SCNView.technique を設定すること。
・ノード毎の見た目を変えるなら、SCNGeometry.shaderModifiers または SCNGeometry.program を設定すること。
・材質毎の見た目を変えるなら、SCNMaterial.program を設定すること。
空間制御の術式に僅かながら心得のあったあなたは、ふとある噂を思い出します。

SceneKitが生み出された当時、UnityやUnreal Engineといった別の術式が隆盛を極めており、SceneKitのような特殊な環境でのみ発動できる術式を好き好んで使う者は少なかったという。その特殊性から、SceneKitに関する資料も少なく、それ故に術者も減少の一途を辿り、現代では術者も資料も失われてしまったとされている。しかし、SceneKitに関する本を書庫に保管している図書館が世界にいくつかあるという噂がある。理由は分からないが、その本は禁書とされ、貸し出しはされていないという。


2017/06/25(Sun) 02:40:34

 Webpack の devServer でファイル毎のContent-Type(mime)を設定する / magicien 

この方法で正しいのかよく分からないけど、とりあえず動いたので忘れないうちにメモしておく。


webpack-dev-serverで、.exr という拡張子のファイルの Content-Type を "image/x-exr" で返したいんだけど、どうすればいいのかな?と思って適当にいじってたらなんか動いた。
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
new WebpackDevServer(webpack(config.webpack), config.webpack.devServer).listen(...)
devServerを初期化するときは、こんな感じで config.webpack.devServer をオプションとして渡しているのだけれど、このオプションをこんな感じで書いた。
{
    staticOptions: {
        setHeaders: (res, path, stat) => {
            if(/.exr$/.test(path)){
                res.setHeader('Content-Type', 'image/x-exr')
            }
        }
    }
}
思いっきり関数埋め込んでますが。まぁ大改造したわけでもないし、これで良しとする。

ブラウザで表示してみて気づいたけど、exrファイル対応してるのSafariだけっぽい?そもそも読み込めたとしてどうやってシェーダに送るかという問題もあるか。データ構造自体はそんなに複雑じゃないっぽいけど、いざパーサを書こうとすると、データが圧縮されてるのがきつい。ぐぬぬ...
2017/06/23(Fri) 14:31:12

 SceneKitの系 / magicien 

人間てぇものは、物事を二つに分類したがる嫌いがあるようでして、「私の彼氏が理系で」「俺の彼女が肉食系で」なんて言ったりするわけでございます。そうやって分類することで、物事をちょっぴり分かった気がするんでしょうな。3Dグラフィックスライブラリなんてわけのわからないものに出くわしたときは、「おや、これは何系かな?」と言っておけば、「おっ、お客さん通だねぇ」なんてことになるわけでございます。


左手系・右手系

3Dのライブラリを分類するとすれば、まずは「左手系」「右手系」だろう。
X軸を右、Y軸を上とした時に、Z軸が手前に来るのが右手系、Z軸が奥に行くのが左手系ということらしい。電磁気学とかだとここら辺が重要かもしれないけれど、3Dだと正直どうでも良い。
OpenGLは右手系、DirectX、MMD、Unityは左手系。SceneKitは内部でOpenGLを使うことがあるので右手系になっている。

行列

SceneKitでは、SCNMatrix4という4x4行列用の構造体がある。
各成分は、m11、m12、...、m44 という名前になっている。配列ではないので、ループで一気に計算、というわけにはいかない。配列にしたらしたでやれ列優先だ、行優先だという話になるので、これはこれで良いと思う。
新APIだと、simd系と相互変換できるようになったので、計算が必要な場合は、一度simdに変換すると良いだろう。
let m1: SCNMatrix4 = ...
let m2: SCNMatrix4 = ...
let m3: SCNMatrix4 = ...
let answer = SCNMatrix4Mult( SCNMatrix4Mult( m1, m2 ), m3 )

let f1 = float4x4(m1).transpose   // float4x4(m1) とすると、行と列が逆になるので転置する
let f2 = float4x4(m2).transpose
let f3 = float4x4(m3).transpose
let f = f1 * f2 * f3

let answer2 = SCNMatrix4( f.transpose )   // answerとanswer2の結果はほぼ一致する

if( Float( m1.m23 ) == f1.cmatrix.columns.2.y ){
    // 上記 if文は概ねtrueになる。が、計算精度の問題で一致しないこともあるだろう。
    // float4x4の場合、列は0123、行はxyzwでアクセスする。列・行の順でわかりづらい。
    // SCNMatrix4.m23 は CGFloat 型なので Float 型にキャストしてから比較している。型の違いは「色」の項目で。
}

回転

3Dで一番厄介なのが、この回転である。どの軸をどの順番でどの方向に回すかで挙動が変わる。各ライブラリがその個性を遺憾無く発揮する場であり、3Dデータ・APIの相互変換を難しくする主因となっている。

SceneKitの場合、SCNNodeの回転を設定するには、変換行列である transformを直接設定する方法以外に、rotation、orientation、eulerAngles の3つの変数を設定する方法がある。どれか一つを変更すれば、他の変数も全て変更される。

rotation: SCNVector4 回転軸(xyz)と回転量(w、ラジアン)を指定
orientation: SCNVector4 いわゆるクォータニオン
eulerAngles: SCNVector3 xyz各軸の回転量をそれぞれラジアンで指定
回転方向は、回転軸の+方向に対して右回り。

回転行列はこんな感じ。

※rotationのxyz軸は大きさ1に正規化される。 \( rotation = (x, y, z, w) \Rightarrow \left( \begin{array}{cccc} x^2 (1-\cos w) + \cos w & xy(1-\cos w) + z\sin w & xz(1-\cos w) - y\sin w & 0 \\ yx(1-\cos w) - z\sin w & y^2(1-\cos w) + \cos w & yz(1-\cos w) + x\sin w & 0 \\ zx(1-\cos w) + y\sin w & zy(1-\cos w) - x\sin w & z^2 (1-\cos w) + \cos w & 0\\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (x, 0, 0) \Rightarrow \left( \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos x & \sin x & 0 \\ 0 & -\sin x & \cos x & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (0, y, 0) \Rightarrow \left( \begin{array}{cccc} \cos y & 0 & -\sin y & 0 \\ 0 & 1 & 0 & 0 \\ \sin y & 0 & \cos y & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

\( eulerAngles = (0, 0, z) \Rightarrow \left( \begin{array}{cccc} \cos z & \sin z & 0 & 0 \\ -\sin z & \cos z & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

eulerAnglesのxyz回転を全て指定した場合は、z軸・y軸・x軸の順番で回転する。
\( eulerAngles = (x, y, z) \Rightarrow \left( \begin{array}{cccc} 1 & 0 & 0 & 0 \\ 0 & \cos x & \sin x & 0 \\ 0 & -\sin x & \cos x & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \left( \begin{array}{cccc} \cos y & 0 & -\sin y & 0 \\ 0 & 1 & 0 & 0 \\ \sin y & 0 & \cos y & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \left( \begin{array}{cccc} \cos z & \sin z & 0 & 0 \\ -\sin z & \cos z & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{array} \right) \)

orientationはrotationとほぼ同じ。関係は次の通り。

\( rotation.xyz = normalize( orientation.xyz ) \)
\( rotation.w = 2 \arccos (orientation.w) \)
※\( \arccos (orientation.w) \) が NaN の場合は、\( rotation.w = 0 \)


ちなみに、MMDのモーションデータの場合、モデルモーションはクォータニオン、カメラモーションはオイラー角で保存されている。MMDのクォータニオンからSceneKitのクォータニオンへの変換は、xとyの符号を反転すれば良い。MMDのオイラー角をSceneKitのオイラー角で表すとすれば、Z軸・-X軸・-Y軸の順で回転するのと同等。

macOS版とiOS版の両方を作る場合は、色にも気をつけないといけない。

SCNMaterialで色を設定する場合、macOSでは、NSColor、iOS/tvOS/watchOSではUIColorを使う。インタフェースはほぼ同じなので、
#if os(macOS)
    typealias Color = NSColor
#else
    typealias Color = UIColor
#endif
という感じで共通化して使ってもいいかもしれない。このとき注意が必要なのは、NSColor・UIColor共通でRGBA成分を表すのに使われているCGFloatという型である。CGFloatは、macOSだと64bit(Double)、iOSだと32bit(Float)になっている。なので、値を代入する時に型変換が必要になったりならなかったりして煩わしい。
実は自前でtypealiasを作らなくても、CGColorやSKColorといった共通クラスが用意されているけれど、CGFloatの差異は吸収されないので煩わしい。
元の型がFloatでもDoubleでもCGFloatにキャストするよう気を付ければ、コンパイラから文句を言われずに済むが、代入のたびにCGFloatとか書かないといけないのがやっぱり煩わしい。

shaderModifiersでシェーダを書く場合、materialに設定した色とシェーダで扱う色で数値が異なるので要注意。シェーダに渡される値は、materialに設定した色を sRGB→RGB 変換したものになるので、数値が小さく(暗く)なる。

2017/06/19(Mon) 02:15:57

 SceneKitでモーフィング / magicien 

MMDで言うところの表情。これをSceneKitではどう実現するか。


SCNMorpherを使うべし。

以前はSCNSkinnerとSCNMorpherを同時に使うと、モーフィングが効かなくなるという問題があったけれど、いつのまにか直ったようだ。
使い方はこんな感じ。
let morpher = SCNMorpher()
let geometries = [SCNGeometry]()
// ここでいろんな形のgeometryを作り、geometriesに入れる。
morpher.targets = geometries
node.morpher = morpher
morpher.setWeight(0.5, forTargetAt: 0)
morpher.setWeight(0.3, forTargetAt: 1)
上記の例だと、元々のnode.geometryが20%、geometries[0]が50%、geometries[1]が30%の割合で頂点座標がブレンドされる。
mopherに設定するgeometryの頂点数は、nodeに設定している元々のgeometryと一致させること。
geometry.geometrySourcesには、vertexの他にnormalも設定しておいた方が良いと思うが、無くても良い。texcoordを設定しておけば、それもブレンドされる。
geometry.geometryElementsは空にしておく。

morpher.setWeightでモーフィングの割合と、targets配列内のインデックス番号を指定する。
MMDの場合、モーフィングのデータは元の頂点座標からの差分データになっていた(x方向に+0.1、y方向に-0.1、z成分はそのまま、等)ので、そのデータをそのままgeometryに設定し、
morpher.calculationMode = .additive
としている。
おそらくモーフィングの計算はCPUで行われているので、ウェイトの大半が0ならば、targetsにたくさんgeometryを設定してもパフォーマンスには影響無さそう(CPU側のメモリに余裕があるなら)。

2017/06/18(Sun) 17:04:35

 SceneKitでスキニングアニメーション / magicien 

MMDで言うと、ボーンを動かす普通のアニメーション。これをSceneKitではどう実現するか。


SCNSkinnerを使うべし。

面倒なのは、SCNSkinnerを作る時に、ボーンのローカル姿勢の逆行列を作って渡してあげないといけないこと。
SCNMatrix4Invert(node.transform) で簡単に作れるので、自分で勝手に作ってくれれば良いと思うのだけれど、node.transformの値が初期姿勢とは限らない、というのを考慮してのことでしょう。面倒ですね。
作るとしたらこんな感じ。
var bones = [SCNNode]()

// ここでbonesに適当にボーンを詰める

var boneInverseBindTransforms = [NSValue]()
for bone in bones {
    boneInverseBindTransforms.append(NSValue(scnMatrix4: SCNMatrix4Invert(bone.transform)))
}

let skinner = SCNSkinner(baseGeometry: node.geometry, bones: bones, boneInverseBindTransforms: boneInverseBindTransforms, boneWeights: boneWeights, boneIndices: boneIndices)
node.skinner = skinner
アニメーションさせる頂点データをbaseGeometryに設定する。
bonesはボーンの配列。順番は、boneInverseBindTransformsと一致させる必要があるし、boneIndicesでも使うので忘れないように。
boneInverseBindTransformsは上記要領で用意してやる。
boneWeights、boneIndicesはSCNGeometrySourceのインスタンス。semanticには、.boneWeights、.boneIndicesをそれぞれ設定する。vectorCountは頂点数と一致させる。componentsPerVectorは1頂点に影響するボーン数の最大値。影響するボーン数はいくつでも設定できるが、5以上にするとGPUで計算できなくなり、CPUを使うので遅くなる。MMDの場合は最大4だと思うので大丈夫。で、boneIndicesの各ベクトルに影響するボーンのbones配列でのインデックス番号を設定し、boneWeightsの各ベクトルに影響する割合を設定すれば良い。言葉で書くとわけがわからん。

node.skinner(baseGeometryで指定したgeometryを持っているノードと一致させる。させないとどうなるかはよくわからん)に作ったSCNSkinnerを設定してやれば、ボーンの動きに合わせて頂点が動くようになる。

2017/06/18(Sun) 16:21:19

 SceneKitのノードをアニメーションさせる方法3:SCNAction / magicien 

SCNActionを使って、SceneKitのノードをアニメーションさせることもできる。

単純にノードを動かしたいなら、これが一番お手軽だと思う。
この場合だと、ノードが3秒に1周のペースでぐるぐる回る。
let action = SCNAction.repeatForever( SCNAction.rotateTo(x: 0.0, y: CGFloat(Float.pi * 2.0), z: 0.0, duration: 3.0) )
node.runAction(action)
どんなアクションが使えるかはこちらを参照のこと。基本的なことはだいたいできる。
timingModeやtimingFunctionを設定してあげれば、補間の仕方が変えられる。


2017/06/18(Sun) 14:48:24