内容に「Pro」を含む
11〜20件目 / 32件
前へ 1 2 3 4 次へ

 Heroku/Node.js/ExpressでGitHub Appsのユーザ認証 / magicien 

HerokuでGitHub Appsのユーザ認証をするサンプルコード。
OAuth2を使ったユーザ認証だけ。JWTを使ってリポジトリを操作するのは別途。


  • セキュリティに関わるコードはなるべく自作しないのがセオリーということでpassport-github2を使ってみる。
  • client_id、client_securityはHerokuの環境変数に設定しておく。
  • GitHubStrategyのオプションに state: true を設定し忘れると、はまちちゃんが遊びに来る。
  • stateには24文字のランダムな文字列が設定される。
  • Herokuの場合、sessionのオプションは secure: 'auto'、proxy: true にしないとCookieが発行されない。参考:connectのセッションミドルウェアのcookieのsecure属性について
  • 各モジュールのバージョン:
    • express:4.15.4
    • express-session:1.15.5
    • passport:0.4.0
    • passport-github2:0.1.10

2017/09/11(Mon) 18:49:06

 Undo/RedoできるCanvasを作った / magicien 

まーた言ったそばから脇道に逸れているわけ。
HTMLCanvasElement (CanvasRenderingContext2D) に undo/redo機能が欲しいな、と思ったので作りました。
undo-canvasデモページ1

CanvasRenderingContext2Dのプロパティに片っ端からフックをかますという荒技を使い、変更したプロパティ、呼び出したメソッドを全て記録しています。
副産物として、上書きしたメソッドを prototype で定義されているものに戻す reset-object を別モジュールとして作りました。

使ってみたい人は、scriptタグでundo-canvasを読み込んでください。
<script src="https://cdn.rawgit.com/magicien/undo-canvas/v0.1.2/undo-canvas.js"></script>
ブラウザ向けnpmモジュールを作っている人は、npmコマンドでインストールできます。
npm install --save undo-canvas
jsファイルを読み込むと、グローバル変数として UndoCanvas が定義されるので、
const canvas = document.getElementById('myCanvas')
const context = canvas.getContext('2d')
UndoCanvas.enableUndo(context)
こんな感じで、UndoCanvas.enableUndoにコンテキストを引数として渡してください。contextに undo/redo 関数が追加され、以降、このcanvasに対する操作が記録されていきます。
undo/redoしたくなったら、
context.undo()
context.redo()
で行ったり来たりできます。
undo/redoが必要なくなったら、disableUndoで無効化できます(記録されていた操作履歴は全て消えます)。
UndoCanvas.disableUndo(context)
その他機能はGithubの記載を更新していくので、そちらを参照してください。

undo/redoを実現する仕組みはOracleのようなDBの考え方をぱく参考にしたのですが、思いの外うまくハマったので、気が向いたら解説記事でも書くかもしれません。

さて、今度こそエディタ制作作業に戻ろう...

2017/09/06(Wed) 04:58:25

 React/Reduxでゲームエディタを作る時のメモ / magicien 

色々と脇道に逸れていたけれど、ゲームエディタを作り始めることにした。
フレームワークは勉強も兼ねて React/Redux を使ってみることにする。この選択が正しいかどうかは分からない。間違っていたら作り直せば良いので気楽に行こう。

以前Reactをほんの少し触ったことはあったけれど、存在意義も含めほぼ理解できずに終わったので、今回は1から勉強し直す。以下、困ったことリスト。回答とか感想とかは未来の自分に任せることにする。React/Reduxの正しい設計が知りたくて見に来た人は参考にしてはいけない。

モジュールのバージョンは?

とりあえず最新のものを使おう。React v15.6.1、Redux v3.7.2。それと、React Routerがバージョンによって大きく挙動が変わるみたい。現状最新の React Router v4.2.0 を使う。色々試した結果、react-routerを使わずに、redux-first-routerを使うことにした。モジュールは随時最新化していくことにする。

デザインをどうするか

色々探してみたけれど、結局Bootstrapを使う。というか、他に選択肢が見つからなかった。自由度も良い感じに制限されていて迷わなくて済むし、もうこれで良いんじゃないかな。デフォルトだとボタンの余白が大きいのが少し気になる。スマホ向けには良さそうだけど、PC向けにはもうちょっと小さくしたい。

各国語対応はどうするか

後になってi18n対応するのは大変そうなので、予め設計を考えておく。react-boilerplateがi18n対応していたので、それを参考に実装する。ソースコードに日本語を埋め込むのも微妙だし、デフォルトの文言は英語にしよう。英語用のメッセージ一覧は無くても良いけれど、他の言語に翻訳する時にメッセージ一覧があった方が便利なので用意する。というか自動生成したい。

ルーティングはどうするか

最初、react-routerを使っていたけれど、URLに応じてコンテンツを変更しようと考えたときに、redux-first-routerの方が楽だということに気づき、乗り換えることにした。そのページを表示する直前・直後に実行する処理も書けるし便利。
redux-first-routerを使うと、ページを移動時にURLに応じたactionがdispatchされるようになる。逆に、actionをdispatchすることでページを移動させることも可能。このactionを使ってURLとstoreをうまいこと同期させてあげれば、コンポーネント側でURLを気にしなくても良くなる。いちいちdispatchしなくてもurlのリンクを書いておけばそれに応じたactionが実行される。便利。
この変更によって、redux-sagaは要らなくなってしまった。

データのフェッチはどうするか

react-boilerplateredux-sagaを使っているので、それに倣ってみる。どうやらGeneratorで実現しているようだ。Sagaの定義はStore/Actionと一緒に置いておく感じかな。actionをdispatchする前にsagaをrunしておく必要があるけど、全ページで全sagaをrunするのは無駄かな。全部runするか、ページ毎に対象を指定するか、うまい方法が無いかは実装しながら考えよう。

今のbabelの設定だと、async/awaitが使えるので、そっちの方が良い気がしてきた。Sagaをrunするとactionの受け取り手が無駄に増えちゃったりするし。GeneratorでもPromiseでも使えるみたいなので、async/awaitに全て移行してしまおう。

テストはどうするか

boilerplateだと各jsファイルの横にtestsディレクトリを作ってテストを書いているようだ。モジュールはJestを使っている。この構成も倣ってみよう。使い方はmocha/chaiとほとんど同じかな?

Undo/Redoはどうするか

これも後から対応するの大変そうだから先に考えておきたい。
大部分のデータは、redux-undoで実現できそう。undo/redoしたい粒度でデータをhistory/futureにとっておく。問題はどういう粒度で何のデータを保存しておくか。エディタのメイン画面はマップエディタにしたいと考えていて、マップの地面の高低はheightmapのテクスチャという形で保存したいと考えている。
この場合、画像編集のデータをどう保存すれば良いのか。画像を丸ごと保存してしまう?グレースケールだから行けそうか...でも変更箇所の差分だけ取っておく方が良いよなぁ。それだとredux-undoの使い方にはマッチしない。Photoshopの実装がどうなっているか知りたい。DBみたいに適当なタイミングでチェックポイント(画像丸ごと保存)を作って、そこからredoが妥当かなぁ。よくよく考えたら、動画フォーマットもそんな感じだよね。でも実装がめちゃくちゃ複雑になりそう。→ Undo/RedoできるCanvasを作った

データの保存はどうするか

一時保存は localStorage IndexedDB へ、永続化はGithubへ、という感じにしたい。Github APIはまだ動作確認中。→GitHub Appsでリポジトリにファイル追加・更新
クライアント側で何を使うかも悩みどころ。IndexedDBはそのままでもシンプルかつ高機能なので、特にラッパー無しでもいけそうな予感。まさか仕様としてブラウザ上にDBが実装されるようになるとは。

ファイル構成をどうするか

Action/Reducer/Selector/Storeを1ファイルにまとめる

React/Reduxの実装例とか見ると、ほとんどの例でファイルが粉々に分割されていて非常に読み辛かった。なので、ducks-modular-reduxを参考に一ファイルにまとめてしまうことにした。テストするときに困るだろうか。

ContainerとグローバルのStateを分離する

ContainerとAction/Reducer/Stateが一対一で対応している例が多かったけれど、その構成のままContainerを100、200と増やしていくと地獄と化すのが目に見えているので、分離することにした。(実装例の規模が小さすぎて1組ずつしか無かっただけかもしれない)
Containerとデータ構造・操作をくっつけると再利用性が上がるのってReduxを使っていない場合の話だよね...?同じデータを複数のContainerで使う時にどう実装すべきか分からない。ContainerにStateを管理させると、設計がぐちゃぐちゃになって拡張が難しくなるようにしか思えない。

最上位のContainer(ページ)は別ディレクトリに分離する

Container同士にも親子関係があるので、同一ディレクトリにフラットに並べると分かりづらい。各画面の最上位のContainerは別ディレクトリに分けることにした。後でページ毎にwebpackするjsファイルを分けるのにも便利じゃないかと思う。

jsのファイル名はindex.jsにする

importするときにファイル名まで書かずに済むから楽なんじゃないかと。

ファイル構成(暫定)

app
├index.html HTMLの外枠。
├app.js  Storeの生成、Router、i18nの仕込み。
├pages  最上位のContainer(ページレイアウト)置き場。
│└pageA
│ ├index.js react-helmetでヘッダ上書き。必要に応じてconnectする。
│ ├Component.js そのページ専用のコンポーネントは一緒に置いてしまう。
│ ├messages.js 翻訳対応するメッセージ一覧
│ └tests
│  ├index.test.js index.jsのテスト
│  └Component.test.js Component.jsのテスト
├containers 各ページ共通で使えそうなパーツ置き場
│└containerA i18n用コンテナとか。
│ ├index.js React.Component等。必要に応じてconnectする。
│ ├messages.js 翻訳対応するメッセージ一覧
│ └tests
│  └index.test.js index.jsのテスト
├components 各ページ共通で使えそうなパーツ置き場
│└componentA ボタンとか。
│ ├index.js React.Componentか関数。connectしない。
│ ├messages.js 翻訳対応するメッセージ一覧
│ └tests
│  └index.test.js index.jsのテスト
├redux
│├configureStore.js Storeを生成。Middleware、Reducer取り込み。
│├reducers.js modulesからreducerを一括でロードする。
│├routesMap.js URLとpagesのマッピングを書く。
││       ページ毎のデータフェッチとかアクセス制御(誘導?)とかも書いておく。
│├modules ActionType/Action/Selector/Reducer置き場
││└moduleA
││ ├index.js 全部まとめて一ファイルに。reducerをexport defaultする。
││ └tests
││  └index.test.js index.jsのテスト
│└tests
│ ├configureStore.test.js 
│ └reducers.test.js
├translations
│├en.json 英語用メッセージ。自動生成したい。
│└ja.json 日本語用メッセージ
├config
│└index.js convictを使って説明とか型とか書いておく。
└tests
 └app.test.js app.jsのテスト

Code Splittingはどうするか

webpackするときに、ページ毎のjsと共通のjsをそれぞれ別ファイルにしたい。
React + Reduxを使った大規模商用サービスの開発」だと、require.ensureを使っていたけれど、例によってReactRouterの仕様変更によりそのままでは使えなかった。
でもReactRouterのページにCode Splittingの方法が書いてあったので、それをそのまま使うことにした。bundle loaderがうまいことやってくれるらしい。生成されるjsファイルは、0.[ハッシュ].chunk.js、1.[ハッシュ].chunk.js みたいになっていて、ページ数が増えると数字がずれたりする。上の資料だと、モジュールの並び順を制御してハッシュ値が変わらないよう工夫していたけれど、今回はそこまで神経質にしなくても良いかな。
react-routerを使うのをやめたので、代わりにuniversal-importを使うことにした。大してコード修正せずにそのまま乗り換えられたので一安心。

Stateに溜まったゴミはどうすればいいのか

画面遷移を続けていくと、Stateにゴミデータがどんどん溜まっていく。データの後処理はどうするのが良いのかな?これもReduxを使うが故の悩みだろうか。

ネット上の情報はいろんなバージョンのReactを使っていてよく分からない

バージョン間の差異が大きすぎる。どうしたらいいんでしょうか。とりあえず半年以上前の記事は話半分で読むことにする。

参考になったもの

React + Reduxを使った大規模商用サービスの開発

画面とかStateとか大量にある場合の設計の実例。実サービスとして作っただけあって、実践的。本格的に作り始める前にこの資料を見つけられて本当に良かった。性能測定はどうやっているのだろう。

ducks-modular-redux

action-types/actions/reducersを別々のファイルにしてもあんまり意味がないから一つのファイルにまとめちゃおうという提案。自分も同じ疑問を持っていたので、しっくりきた。

react-boilerplate

実際にReact/Reduxのアプリを作る上で最小限必要なものが一通り揃っている。i18n対応しているのが非常に素晴らしい。Herokuへのデプロイに対応しているのもポイント高い。React Router v3だったので、残念ながらそのまま使うことはできなかったけれど、大いに参考になった。

wp-calypso

WordPressのReact/Redux実装。大規模アプリの実例として参考になった。規模が大きすぎる気がしなくもない。ライセンスがGPLなので要注意。

2017/09/02(Sat) 18:32:24

 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

 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

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

SceneKitのノードをアニメーションさせる方法として、Core Animationを使用することもできる。

公式の説明から例を引用すると、
let animation = CABasicAnimation(keyPath: "geometry.extrusionDepth")
animation.fromValue = 0.0
animation.toValue = 100.0
animation.duration = 1.0
animation.autoreverses = true
animation.repeatCount = .infinity
textNode.addAnimation(animation, forKey: "extrude")
こんな感じ。CABasicAnimationやCAPropertyAnimationの場合は、どの値を変更するかをkeyPathで指定する。
上記の場合、textNode.geometry.extrusionDepthの値が0.0から100.0へ、1秒間かけて変更され、その後、1秒間かけて100.0から0.0に戻される。以降、繰り返し。
addAnimationの時に指定するキーは、アニメーションを止めたり消したりする時に使うためのものなので、追加した後、特に制御する予定がなければ nil でも良い。
repeatCountが .infinity でない場合は、アニメーション終了時に自動的にアニメーションが削除される。
アニメーションが削除されると、アニメーション適用前の値に戻ってしまうので、要注意。(SCNTransactionの場合は、アニメーション後も値は変更されたまま)
アニメーション終了時の値のままにしたい場合は、アニメーションが消えないように、
animation.isRemovedOnCompletion = false
を設定してやると良い。ただ、手動でアニメーションを削除してしまえば、やっぱり値は元に戻ってしまう。
アニメーションが消えても値を維持したいなら、アニメーション終了時に値を代入してやれば良い。
func hoge() {
    ....
    animation.delegate = self
    ....
}

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if(flag){
        // アニメーション終了時にここが実行されるので、改めて値を入れてあげたり。
    }
}
新APIだと、Core Animationの他に、SCNAnimationというのが使えるようになったらしい。
また、addAnimationの代わりに、addAnimationPlayerを使うのが推奨されている。(まだ試してない)
使うとしたらこんな感じ。
let animation = SCNAnimation(caAnimation: CABasicAnimation(...)) // 結局中身はCAAnimation
animation.animationDidStop = { (animation: SCNAnimation, obj: SCNAnimatable, finished: Bool) in
    // 終了時の処理。delegate使うより個別に設定できるし使いやすそう。
}
let player = SCNAnimationPlayer(animation: animation)
node.addAnimationPlayer(player, forKey: nil)
player.play()
addAnimationだと、追加した瞬間にアニメーション開始しちゃったけど、SCNAnimationPlayerを使えば、好きなタイミングで開始できてちょっと便利。

MMDみたいなキーフレームアニメーションの場合は、CAKeyframeAnimationを使って実現できる。また、複数のアニメーションを同期させる場合は、CAAnimationGroupを使う。

ちなみに、アニメーション中の値を取得する場合は、node.presentation を参照する必要がある。
let animation = CABasicAnimation(keyPath: "position.x")
animation.toValue = 1.0
animation.duration = 2.0

node.position.x = 0.0
node.addAnimation(animation, forKey: nil)
例えば、上記コードを実行した1秒後に node.position.x の値を取得しても 0.0 のままになっている。
node.presentation.position.x の値を取得すると 0.5 になっている。
presentationの値は直接変更できない(できるかもしれないけど、変更するとSceneKitが困ってしまう)ので、値を変えたい場合は、通常通り node のパラメータを変更すること。

2017/06/18(Sun) 14:27:30

 JavaScriptでテキストデータの読み込み / magicien 

JavaScriptでXファイルを読み込んだときに使った方法。

バイナリデータの読み込みについて書きかけの状態ですが、今回はテキストデータの読み込みについて。
実行速度よりも実装のしやすさを優先するなら、Ginがおすすめ。
実際の使い方としてはこんな感じ。
var xfileGrammar;
function start()
{
  xfileGrammar = new Gin.Grammar({
  XFile:         / XFileHeader:header (XObjectLong)* /,
  XFileHeader:   / 'xof ':xof VersionNumber FormatType:showValue FloatSize:showValue /,
  VersionNumber: / <\d\d>:showValue <\d\d>:showValue /,
  FormatType:    / 'txt ' | 'bin ' | 'tzip' | 'bzip' /,
  FloatSize:     / '0064' | '0032' /,
 
  XObject:       / Template | Animation | AnimationKey | AnimationOptions | AnimationSet | AnimTicksPerSecond | Boolean | Boolean2d | ColorRGB | ColorRGBA | Coords2d | DeclData | EffectDWorld | EffectFloats | EffectInstance | EffectParamDWorld | EffectParamFloats | EffectString | FaceAdjacency | FloatKeys | Frame | FrameTransformMatrix | FVFData | Header | IndexedColor | Material | Matrix4x4 | MeshFaceWraps | MeshFace | MeshMaterialList | MeshNormals | MeshTextureCoords | MeshVertexColors | Mesh | Patch | PatchMesh9 | PatchMesh | PMAttributeRange | PMInfo | PMVSplitRecord | SkinWeights | TextureFilename | TimedFloatKeys | Vector | VertexDuplicationIndices | VertexElement | XSkinMeshHeader /,
  XObjectLong:   / Template | AnimationLong | AnimationKeyLong | AnimationOptionsLong | AnimationSetLong | AnimTicksPerSecondLong | BooleanLong | Boolean2dLong | ColorRGBLong | ColorRGBALong | Coords2dLong | DeclDataLong | EffectDWorldLong | EffectFloatsLong | EffectInstanceLong | EffectParamDWorldLong | EffectParamFloatsLong | EffectStringLong | FaceAdjacencyLong | FloatKeysLong | FrameLong | FrameTransformMatrixLong | FVFDataLong | HeaderLong | IndexedColorLong | MaterialLong | Matrix4x4Long | MeshFaceWrapsLong | MeshFaceLong | MeshMaterialListLong | MeshNormalsLong | MeshTextureCoordsLong | MeshVertexColorsLong | MeshLong | PatchLong | PatchMeshLong | PatchMesh9Long | PMAttributeRangeLong | PMInfoLong | PMVSplitRecordLong | SkinWeightsLong | TextureFilenameLong | TimedFloatKeysLong | VectorLong | VertexDuplicationIndicesLong | VertexElementLong | XSkinMeshHeaderLong /,
 
  XString:       / <[0-9A-Za-z]*> /,
  Identifier:    / XString /,
  Name:          / XString /,
  UUID:          / '<' (<[0-9A-Za-z]|->)* '>' /,
  Member:        / ('array' TemplateType Name '[' ($INT | XString) ']') | (TemplateType Name) /,
  TemplateType:  / XString /,
  XArray:        / '[' $INT ']' /,
 
  XObjectTemplate:       / Identifier:identifier Name:name '{' UUID ('[...]' | ('[' Name ']') | (Member ';'))* '}' /,
  Template:             / 'template':template  Name '{' UUID ('[...]' | ('[' Name ']') | (Member ';'))* '}' /,
 
  IntValue:             / $INT ';' ::first /,
  IntArray:             / $INT (',' $INT)* (',')? ';' /,
 
  FloatValue:           / $REAL ';' ::first /,
  FloatArray:           / $REAL (',' $REAL)* (',')? ';' /,
 
  Header:               / HeaderLong | (HeaderValue ';') /,
  HeaderLong:           / 'Header' (Name)? '{' (UUID)? HeaderValue '}' /,
  HeaderValue:          / IntValue:major IntValue:minor IntValue:flags /,
 
  Animation:            / AnimationLong | ( AnimationValue ';') /,
  AnimationLong:        / 'Animation' (Name)? '{' (UUID)? AnimationValue '}' /,
  AnimationValue:       / $ANY /,
 
  AnimationKey:         / AnimationKeyLong | ( AnimationKeyValue ';') /,
  AnimationKeyLong:     / 'AnimationKey' (Name)? '{' (UUID)? AnimationKeyValue '}' /,
  AnimationKeyValue:    / $ANY /,
 
  AnimationOptions:      / AnimationOptionsLong | ( AnimationOptionsValue ';') /,
  AnimationOptionsLong:  / 'AnimationOptions' (Name)? '{' (UUID)? AnimationOptionsValue '}' /,
  AnimationOptionsValue: / $ANY /,
 
  AnimationSet:         / AnimationSetLong | ( AnimationSetValue ';') /,
  AnimationSetLong:     / 'AnimationSet' (Name)? '{' (UUID)? AnimationSetValue '}' /,
  AnimationSetValue:    / $ANY /,
 
  AnimTicksPerSecond:      / AnimTicksPerSecondLong | ( AnimTicksPerSecondValue ';') /,
  AnimTicksPerSecondLong:  / 'AnimTicksPerSecond' (Name)? '{' (UUID)? AnimTicksPerSecondValue '}' /,
  AnimTicksPerSecondValue: / $ANY /,
 
  Boolean:              / BooleanLong | ( BooleanValue ';') /,
  BooleanLong:          / 'Boolean' (Name)? '{' (UUID)? BooleanValue '}' /,
  BooleanValue:         / $ANY /,
 
  Boolean2d:            / Boolean2dLong | ( Boolean2dValue ';') /,
  Boolean2dLong:        / 'Boolean2d' (Name)? '{' (UUID)? Boolean2dValue '}' /,
  Boolean2dValue:       / $ANY /,
 
  ColorRGB:             / ColorRGBLong | (ColorRGBValue ';') /,
  ColorRGBLong:         / 'ColorRGB' (Name)? '{' (UUID)? ColorRGBValue '}' /,
  ColorRGBValue:        / FloatValue:red FloatValue:green FloatValue:blue /,
 
  ColorRGBA:            / ColorRGBALong | (ColorRGBAValue ';') /,
  ColorRGBALong:        / 'ColorRGBA' (Name)? '{' (UUID)? ColorRGBAValue '}' /,
  ColorRGBAValue:       / FloatValue:red FloatValue:green FloatValue:blue FloatValue:alpha /,
 
  Coords2d:             / Coords2dLong | (Coords2dValue ';') /,
  Coords2dLong:         / 'Coords2d' (Name)? '{' (UUID)? Coords2dValue '}' /,
  Coords2dArray:        / Coords2dValue (',' Coords2dValue)* (',')? ';' /,
  Coords2dValue:        / FloatValue:u FloatValue:v /,
(中略)
  XSkinMeshHeader:      / XSkinMeshHeaderLong | ( XSkinMeshHeaderValue ';') /,
  XSkinMeshHeaderLong:  / 'XSkinMeshHeader' (Name)? '{' (UUID)? XSkinMeshHeaderValue '}' /,
  XSkinMeshHeaderValue: / $ANY /,
}, "XFile", Gin.SPACE);

function XFileHandler() { this._s = [] };
XFileHandler.prototype = {
  first: function(v){ return v[0]; },
  header: function(v){ alert("version: " + v[1]); },
  identifier: function(v){ alert("identifier:" + v[0]); },
  xof: function(v){ alert("xof"); },
  showValue: function(v){ alert("value:" + v); },
  name: function(v){ alert("name:" + v); },
(中略)
  // Mesh
  mesh:      function(v){ alert("Mesh start"); },
  nVertices: function(v){ alert("nVertices: " + v); },
  vertices:  function(v){},
  nFaces:    function(v){ alert("nFaces: " + v); },
  faces:     function(v){},
(中略)
};
 
var match;
function parseXFile(request)
{
  var text = request.responseText;
  var handler = new XFileHandler();
  match = xfileGrammar.parse(text, handler);
  if(match && match.full) {
    alert("OK");
  }else{
     alert("NG");
  }
}

var url = "xfile.x";
var myAjax = new Ajax.Request(
  url,
  {
    method: 'get',
    onComplete: parseXFile
  });
}
パースしたいデータの構文が分かっているなら、それをそのまま書いてやれば簡単にパーサが実装できる。
速度を優先するなら、各ファイル形式に合わせて自分で実装すべし。
xファイルのパーサはこんな感じで作りました。
var XParser = Class.create({
  _text: null,
(中略)
  parse: function(text){
    this._text = text;
    if(!this._obj){
      this._obj = new XModel();
    }
    this._offset = 0;
    this._err = 0;

    if(!this.XFileHeader(this._obj)){
      alert("header format error");
      this._err = 1;
      return null;
    }
    while(this.XObjectLong(this._obj)){}
    if(this._err){
      alert("obj format error:" + this._err);
      return null;
    }
    this.splitFaceNormals();

    return this._obj;
  },

  moveIndex: function(len) {
    this._text = this._text.substring(len);
    this._offset += len;
  },

  getString: function(pattern) {
    this.skip();
    var str = this._text.match(pattern);
    if(str != null){
      this.moveIndex(str[0].length);
      return str[0];
    }
    return null;
  },

  /* matching patterns */
  skip: function() {
    var i=0;
    var code;
    while(1){
      code = this._text.charCodeAt(i);
      if(code != 32 && (code < 9 || code > 13)){
        break;
      }
      i++;
    }
    if(i>0){
      this.moveIndex(i);
    }
  },

  _integerPattern: new RegExp(/^(-|\+)?\d+;?/),
  getInteger: function() {
    var str = this.getString(this._integerPattern);
    var val = parseInt(str);
    return val;
  },

  _floatPattern: new RegExp(/^(-|\+)?(\d)*\.(\d)*;?/),
  getFloat: function() {
    var str = this.getString(this._floatPattern);
    var val = parseFloat(str);
    return val;
  },
(中略)
  XObjectLong: function(parent){
    var id = this.getWord();
    if(id == null){
      return null;
    }
    switch(id){
      case "template":
        return this.Template(parent);
      case "Header":
        return this.Header(parent);
      case "Mesh":
        return this.Mesh(parent);
      case "MeshMaterialList":
        return this.MeshMaterialList(parent);
      case "MeshNormals":
        return this.MeshNormals(parent);
      case "MeshTextureCoords":
        return this.MeshTextureCoords(parent);
      case "MeshVertexColors":
        return this.MeshVertexColors(parent);

      default:
        alert("unknown type:" + id);
        break;
    }
    return false;
  },
(中略)
  Mesh: function(parent) {
    this.getLeftBrace();

    // vertices
    var nVertices = this.getInteger();
    var skinArray = $A();
    for(var i=0; i<nVertices; i++){
      var skin = new Skin();
      var pos = new DHVector3();
      pos.x = this.getFloat();
      pos.y = this.getFloat();
      pos.z = -this.getFloat();
      skin.position = pos;

      skin.boneIndex[0] = 0;
      skin.boneIndex[1] = -1;
      skin.boneIndex[2] = -1;
      skin.boneIndex[3] = -1;

      skin.skinWeight[0] = 1;
      skin.skinWeight[1] = 0;
      skin.skinWeight[2] = 0;
      skin.skinWeight[3] = 0;

      skinArray.push(skin);

      this.getCommaOrSemicolon();
    }
    this._obj.skinArray = skinArray;
    this._obj.dynamicSkinOffset = -1;

    // faces
    var nFaces = this.getInteger();
    var faces = $A();
    for(var i=0; i<nFaces; i++){
      var face = this.getIntegerArray();
      faces.push(face);
    }
    this._indices = faces;
    this.getRightBrace();

    return true;
  },
(中略)
}

2012/10/05(Fri) 01:21:39

 JavaScriptでバイナリデータの読み込み / magicien 

JavaScriptでPMD、VMD等のバイナリデータを読み込んだときに使った方法。

HTML5のAPIの実装が進めば、こんなことする必要も無くなる気がしますが、今使っている方法は次のとおり。

まずはファイルをバイナリとして取得する必要がある。
読み込みの対象がFileオブジェクトの場合は、FileReaderを使えばOK。
function readData(data) {
 ...
}

function readBinaryFile(file) {
  var reader = new FileReader();
  reader.onloadend = function() {
    readData(reader.result);
  };
  reader.readAsBinaryString(file);
}
読み込み対象がサーバ上のファイルであれば、"charset=x-user-defined" として読み込む。
例によって prototype.js を使っているので、Ajax.Requestのサブクラスを作って次のようにした。
var BinaryRequest = Class.create(Ajax.Request,
  initialize: function($super, url, options) {
    $super(url, options);
  },

  request: function() {
    this.transport.overrideMimeType('text/plain; charset=x-user-defined');
    Ajax.Request.prototype.request.apply(this, argument);
  },
});

function readBinaryFileFromURL(url) {
  new BinaryRequest(url, {
    method: 'GET',
    onComplete: function(response) {
      readData(response.responseText);
    },
  });
}
これで、データを読み込むところまでは出来たので、あとはデータの解析をするだけ。
とはいえ、そっちの方が大変ですが。データの解析はまた今度。
2012/09/29(Sat) 01:06:13

 WebGLでの文字描画 / magicien 

WebGLで文字を表示させようとすると、いろいろと面倒なので、そのときに使った方法。

2Dの文字や図形を描画する場合、WebGLでまともに実現しようとすると、canvasのオブジェクトをもう一つ作って、字を描いて、テクスチャに貼付けて、それをレンダリングする、というような流れになるかと思います。
が、わざわざそんなことしなくても、作ったcanvasをそのままHTMLの画面上に貼付けた方が手間がかからないし、描画も早かったので、下のようなコードで3Dのcanvasの上に2Dのcanvasを同じサイズ、位置で重ねて使いました。
function create2DContext(canvas3D) {
  var canvas2D = document.createElement('canvas');
  canvas3D.insert( { after: canvas2D } );
  Element.setStyle( canvas2D, {
    position: 'absolute',
    'z-index': 10
  });
  Position.clone( canvas3D, canvas2D );
  canvas2D.width = canvas3D.width
  canvas2D.height = canvas3D.height

  var context2D = canvas2D.getContext('2d');
  return context2D;
}
prototype.js、scriptaculousを使っているので、ソースはそのまま使えないかもですが...
1つのcanvasで2Dのコンテキストと3Dのコンテキストを両方取得できれば問題無いのですが、一度どちらかのコンテキストを取得してしまうと、もう一方のコンテキストが取得できなくなってしまうため、2D用canvasと3D用canvasの2つが必要になるわけです。
2012/09/23(Sun) 18:06:49