Metasequoia station plugin "ObjectComposition ver2"


準備に手間取りました。
以前のものの上位版になります。


オブジェクトを立方体に変形投影するMetasequoiaプラグインです。
同じ形状のものを何個もクローンさせる場合に効果を発揮します。
投影なので配置後も元オブジェクトを修正するとすべてに反映されます。
8頂点(1x1x1)、28頂点(2x2x2)の立方体に対応、また入子型(コンポ内コンポ)にも対応しました。
windows 32bit(x86),x64(x64)にて動作します。

ダウンロード

DropBoxよりダウンロードください。
https://www.dropbox.com/s/in3w93ssgjmhi9z/mqdll_ObjectComposition.zip?dl=0

github

ソースコードgithubに公開しておきます。
https://github.com/Ko-Ta2142/mq_ObjectComposition
コンパイルはVisualStudio2015で行っています。


あとでまた整理、書き足します。

ローカルなんとか

c++で関数内関数を使いたいんじゃ!

int hoge::hoge(){
    struct Inner{
        int addadd(int a){
            return a+a;
        }
    }
    return Inner::addadd(4);
}

こんなことって普通は無くて、クラスメソッドが使いたいんじゃ!

int hoge::hoge(){
    struct Inner{
        int addadd(const hoge* me,int a){
            return me->hogehoge(a) + me->hogehoge(a);
        }
    }
    return Inner::addadd(&this,4);
}

コンパイラ「そのメソッドはpublicではありません」。
そっかーやめとくか。

4x4matrixのちょっとした最適化

matrix計算も思い出すのに30分かかりました。もう6〜8年前のコードだしそりゃ忘れるって話ですよ…。

行列の結合

mulとかcompositeとか色々言われる、行列同士のかけ算です。

function matrix4composite(m1,m2)
	local t = {
		m1[1+0]*m2[1+0] + m1[2+0]*m2[1+4] + m1[3+0]*m2[1+8] + m1[4+0]*m2[1+12],
		m1[1+0]*m2[2+0] + m1[2+0]*m2[2+4] + m1[3+0]*m2[2+8] + m1[4+0]*m2[2+12],
		m1[1+0]*m2[3+0] + m1[2+0]*m2[3+4] + m1[3+0]*m2[3+8] + m1[4+0]*m2[3+12],
		m1[1+0]*m2[4+0] + m1[2+0]*m2[4+4] + m1[3+0]*m2[4+8] + m1[4+0]*m2[4+12],

		m1[1+4]*m2[1+0] + m1[2+4]*m2[1+4] + m1[3+4]*m2[1+8] + m1[4+4]*m2[1+12],
		m1[1+4]*m2[2+0] + m1[2+4]*m2[2+4] + m1[3+4]*m2[2+8] + m1[4+4]*m2[2+12],
		m1[1+4]*m2[3+0] + m1[2+4]*m2[3+4] + m1[3+4]*m2[3+8] + m1[4+4]*m2[3+12],
		m1[1+4]*m2[4+0] + m1[2+4]*m2[4+4] + m1[3+4]*m2[4+8] + m1[4+4]*m2[4+12],

		m1[1+8]*m2[1+0] + m1[2+8]*m2[1+4] + m1[3+8]*m2[1+8] + m1[4+8]*m2[1+12],
		m1[1+8]*m2[2+0] + m1[2+8]*m2[2+4] + m1[3+8]*m2[2+8] + m1[4+8]*m2[2+12],
		m1[1+8]*m2[3+0] + m1[2+8]*m2[3+4] + m1[3+8]*m2[3+8] + m1[4+8]*m2[3+12],
		m1[1+8]*m2[4+0] + m1[2+8]*m2[4+4] + m1[3+8]*m2[4+8] + m1[4+8]*m2[4+12],

		m1[1+12]*m2[1+0] + m1[2+12]*m2[1+4] + m1[3+12]*m2[1+8] + m1[4+12]*m2[1+12],
		m1[1+12]*m2[2+0] + m1[2+12]*m2[2+4] + m1[3+12]*m2[2+8] + m1[4+12]*m2[2+12],
		m1[1+12]*m2[3+0] + m1[2+12]*m2[3+4] + m1[3+12]*m2[3+8] + m1[4+12]*m2[3+12],
		m1[1+12]*m2[4+0] + m1[2+12]*m2[4+4] + m1[3+12]*m2[4+8] + m1[4+12]*m2[4+12]
	}
	return t
end

すごい…目がチカチカする。
forで回そうが焼け石に水です。
これだと新たな行列が生成されて毎回メモリを食っちゃうので、以下のように一個目に反映させてecoしたりします。

function matrix4pushcomposite(m1,m2)
	local h1,h2,h3,h4

	h1 = m1[1+0]
	h2 = m1[2+0]
	h3 = m1[3+0]
	h4 = m1[4+0]
	m1[1+0] = h1*m2[1+0] + h2*m2[1+4] + h3*m2[1+8] + h4*m2[1+12]
	m1[2+0] = h1*m2[2+0] + h2*m2[2+4] + h3*m2[2+8] + h4*m2[2+12]
	m1[3+0] = h1*m2[3+0] + h2*m2[3+4] + h3*m2[3+8] + h4*m2[3+12]
	m1[4+0] = h1*m2[4+0] + h2*m2[4+4] + h3*m2[4+8] + h4*m2[4+12]

	h1 = m1[1+4]
	h2 = m1[2+4]
	h3 = m1[3+4]
	h4 = m1[4+4]
	m1[1+4] = h1*m2[1+0] + h2*m2[1+4] + h3*m2[1+8] + h4*m2[1+12]
	m1[2+4] = h1*m2[2+0] + h2*m2[2+4] + h3*m2[2+8] + h4*m2[2+12]
	m1[3+4] = h1*m2[3+0] + h2*m2[3+4] + h3*m2[3+8] + h4*m2[3+12]
	m1[4+4] = h1*m2[4+0] + h2*m2[4+4] + h3*m2[4+8] + h4*m2[4+12]
...省略
end

そろそろhatenaさんに怒られちゃう文字数なので省略します。
あんまり変わりませんが、実際の速度はこれで結構マシになったりします。以上でお膳立ては完了です。

回転行列と結合したい

本題。回転を行列に加えたいなって場合は以下のような感じで組むと思います。

local matrix1 = matrix4indentity() --単位行列
matrix4pushcomposite(matrix1 , matrix4rotatex(rotx)) --X軸回転を反映

毎回行う行列の結合が重いのなんのって。
なんですがX軸回転の行列なんて

function matrix4rotatex(r)
  local s = math.sin(r)
  local c = math.cos(r)
  local m2 = {}
  m2[1+0] = 1.0   m2[2+0] = 0.0   m2[3+0] = 0.0   m2[4+0] = 0.0
  m2[1+4] = 0.0   m2[2+4] = c     m2[3+4] = s     m2[4+4] = 0.0
  m2[1+8] = 0.0   m2[2+8] = -s    m2[3+8] = c     m2[4+8] = 0.0
  m2[1+12] = 0.0  m2[2+12] = 0.0  m2[3+12] = 0.0  m2[4+12] = 1.0
  return m2
end

と、縦に見て、2行目と3行目ぐらいしか使ってませんし、1行目と4行目は単位行列です。
最適化しましょう。
まず、回転と結合を1つの関数にまとめます。

function matrix4pushrotatex(m1,x)
  local s = math.sin(r)
  local c = math.cos(r)
  local m2 = {}
  m2[1+0] = 1.0   m2[2+0] = 0.0   m2[3+0] = 0.0   m2[4+0] = 0.0
  m2[1+4] = 0.0   m2[2+4] = c     m2[3+4] = s     m2[4+4] = 0.0
  m2[1+8] = 0.0   m2[2+8] = -s    m2[3+8] = c     m2[4+8] = 0.0
  m2[1+12] = 0.0  m2[2+12] = 0.0  m2[3+12] = 0.0  m2[4+12] = 1.0
  return m2
  
  local h1,h2,h3,h4
  h1 = m1[1+0]
  h2 = m1[2+0]
  h3 = m1[3+0]
  h4 = m1[4+0]
  m1[1+0] = h1*m2[1+0] + h2*m2[1+4] + h3*m2[1+8] + h4*m2[1+12]
  m1[2+0] = h1*m2[2+0] + h2*m2[2+4] + h3*m2[2+8] + h4*m2[2+12]
  m1[3+0] = h1*m2[3+0] + h2*m2[3+4] + h3*m2[3+8] + h4*m2[3+12]
  m1[4+0] = h1*m2[4+0] + h2*m2[4+4] + h3*m2[4+8] + h4*m2[4+12]
...省略
end

バーン。でかい…省略するね。
1行目と2行目は使ってないので削除しましょう。ついでにsin,cosも直接指定します。

function matrix4pushrotatex(m1,x)
  local s = math.sin(r)
  local c = math.cos(r)
  local m2 = {}
  -- m2[2+4] = c   m2[3+4] = s
  -- m2[2+8] = -s  m2[3+8] = c

  local h1,h2,h3,h4
  h1 = m1[1+0]
  h2 = m1[2+0]
  h3 = m1[3+0]
  h4 = m1[4+0]
  m1[2+0] = h1*m2[2+0] + h2*c + h3*-s + h4*m2[2+12]
  m1[3+0] = h1*m2[3+0] + h2*s + h3*c + h4*m2[3+12]
...省略
end

回転行列の残りの部分は0.0ですから、あらこれイラネ。

function matrix4pushrotatex(m1,x)
  local s = math.sin(r)
  local c = math.cos(r)

  local h2,h3
  h2 = m1[2+0]
  h3 = m1[3+0]
  m1[2+0] = h2*c + h3*-s
  m1[3+0] = h2*s + h3*c
...省略
end

となります。省略部分もこれと全く同じように最適化してできあがりです。
以上はx軸回転ですが、あとy軸、z軸、スケール、移動に関しても、全く同様の点順で最適化出来ます。
もっとケースを絞り込めば最適化も出来ますが、とりあえず汎用的に使うならこれぐらいでしょうか。

github


https://github.com/Ko-Ta2142/lua_matrix
適当においておきます。適当にお使いください。
クォーターニオンとかはありません。掃きだめにそこまで求めないでください:Q

githubのtab値かえられるの!?

urlに「?ts=4」で変えられるんですね。
しらなかった
そんなの

ところでconsolas

win10になってからconsolasフォントは綺麗ですよね。VScodeでフォントとしての格を上げました。
そんなconsolasなんですが、多のエディタやHTMLで指定すると日本語フォントがイマイチ…VScodeのように綺麗に表示してくれません。(ものによってはおまえはプロポーショナルを強引に固定してるのかってぐらい文字が左右に寄っちゃったり…)
VScodeの設定では以下のような感じで日本語設定は有りません。

Consolas, 'Courier New', monospace

一応windows.FontLinkレジストリを見てみても、リンク定義はされていません。
YuGothicでもないような綺麗さですし…。

正解はメイリオでした。
あれ?でも自分はwin7からのアプグレ組だから良いけど、win10クリーンインストールだと無いんじゃ…。

luaの内部最適化について

これメモリ確保してる?無駄に新しいの生成してない?など、luaの内部挙動についてマニュアルを見ても不明だった点を2,3書き残しておきたいと思います。
内部処理は今風でとても賢いです:Q

文字列の受け渡し

luaの引数の説明では「参照渡しされるのはテーブル」と表記されていて文字列に関しては明記されていません。てっことはもしかしてコピーされちゃうの?
というわけで、allocをフックしてメモリ確保の流れをログします。

local str = "test case !!!!!!!!!!!!!!!!"
custom.alloclog(true)
testcall(str)
print(str)
custom.alloclog(false)

function testcall(str)
  print(str)
end

custom.alloclog()はメモリ確保、解放ログ出力の自前命令、仮命令です。関数の呼び出し時にコピーされるか確認します。
一応参照渡しチェックと言うことで、元strの内容も出力しておきましょうか。

test case !!!!!!!!!!!!!!!!
test case !!!!!!!!!!!!!!!!

メモリ確保ログが出ないのでコピーされていません。”低レベル層”では参照渡しがされているのがわかります。
というわけで内部で連結してみましょう。

function testcall(str)
  str = str.."hoge!"
  print(str)
end
ptr:20890C0 osize:133 nsize:138 usage:244K
ptr:23AB9B0 osize:4 nsize:155 usage:244K
test case !!!!!!!!!!!!!!!!hoge!
test case !!!!!!!!!!!!!!!!

内部連結分の確保が現れましたが大元strの内容はちゃんと維持されますので、”高レベル層”では値渡しでコピーされているかのような振る舞いです。
ちゃんと今風な言語の文字列の動き(値渡しのような振る舞いが出来る文字列)で賢い文字列でした。ありがたい!
文字列においては変更が無い限り生成が発生しないので、数字などと同じコスト(リスク無し)で渡せます。
なので、

function hogeclass:getdata()
  return self.data
end
function hogeclass:setdata(d)
  self.data = d
end

というような、dataが100MBをこえる大きなバイナリデータのやりとりにgetter/setterを挟んでもコストが発生することはありません。安心安全。今すぐインストールです。


おまけ。
luaコンパイルにはちゃんと固定値(const)判断っぽいようなのもあるようで、

custom.alloclog(true)
local str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
custom.alloclog(false)

このコードが実行されるときに"aaaaaaa...."のメモリが確保されることはありません。低レベル層では固定値"aaaaaaaaaaa...."の参照渡しで処理されます。
なので、関数の外に固定値として「const_name="hogehoge"」として宣言しなくても最適化してくれるようです。
ただし、テーブルはその限りではありませんのでご注意ください。

functionのキャッシュ

次はfunctionのコンパイル挙動について。
自分がよく使って怒られる、関数内関数みたいなものの場合のコストについてです。

function hoge(x1)
  function inhoge(x1,x2)
    return x1*x2
  end

  return inhoge(x1+1,x1+2)
end

もうちょっと低レベルに展開すると

function hoge(x1)
  local inhoge = function(x1,x2)
    return x1*x2
  end

  return inhoge(x1+1,x1+2)
end

となり、inhogeに無名関数が突っ込まれる形になります。こうしてみると、hoge()が呼ばれるごとにinhoge()のコンパイルが発生しそうですよね。
それを調べるためにinhoge()の関数ポインタを出力させます。

function hoge(x1)
  local inhoge = function(x1,x2)
    return x1*x2
  end
  print( tofunction(inhoge) )
  return inhoge(x1+1,x1+2)
end
function: 023D3998
function: 023D3998
function: 023D3998
function: 023D3998

全部同値が返ってきたので、function(関数)にはちゃんと使い回せるようにキャッシュみたいなのがあるようです。賢い。
えー同じになってるだけじゃ無いの〜と言うわけで100000ループテストで、関数を外に出した場合(読込時にコンパイルされる位置)と比較してもほぼ同値でしたので問題ないでしょう…。
これで安心して関数内関数をご使用ください。

luaのメモリローダー

lua5.1も気がつけば5.3です。luaに関してはまだ若かりし5.1の頃の記事が残ってますが、良い加減ちゃんと書き直したいので1,2,3を飛ばしてloader周りから書き直したいと思います。

オンメモリ読み込み

組み込みで避けて通れないのがオンメモリ読み込みとその実装です。ファイルから読んじゃダメってやつです。デバッグは良いんですけどね。
luaの場合、スクリプト読み込みは「lua_load」を使って任意ストリーム(データを返す関数ポインタ)でロードできます。任意ポインタを関数callback時に渡せるので…
と思ったらもっと便利な「luaL_loadbuffer」があるのでこっち使えば良いですね。なんだよーもうloadでつくっちゃったよー。

  • luaL_loadbuffer

http://milkpot.sakura.ne.jp/lua/lua52_manual_ja.html#luaL_loadbuffer
ちなみに、loadstringという手もあるのですが、これだとチャンク名が設定できない関係上デバッグの際に読み込みファイル名がゲットできないという致命傷がございますのでご注意くださいませ。なんせデバッグは重要ですので。

それrequireだとダメですよね

はいそうです。これだと内部requireのロードが拾えません。なので、まず読み込むファイル、そしてrequireのローダを乗っ取るの2弾構えが必要です。
本家マニュアルによりますと

  package.searchers
このテーブルの各エントリは 検索関数 です。 モジュールを検索するとき、 require はモジュール名 (require に指定した引数) を引数にしてこれらの各検索関数を昇順で呼びます。 検索関数は別の関数 (モジュールの ローダー) およびそのローダーに渡される追加の引数、 またはモジュールを見つけられなかった理由を説明する文字列 (または何も言うべきことがなければ nil) を返すことができます。

任侠沈没に言ってわからん。
要約すると、requireで指定した文字列を渡すので、それをチャンク(load)にして返してください。エラーならそのメッセージを返してくださいということです。
言ってるかな?…言ってるかも…。

function onmemoryloader(modulename)
  local filename = modulename..".lua"
  local data = custom.load(filename)
  local chank,msg = load(data,filename)
  if chank~=nil then
    return chank
  else
    return msg
  end
end

最小構成はこんな感じかな?こんな感じかも。
「custom.load()」は仮の組み込み関数で、ファイル名を指定するとその内容の(nilを含んだ)文字列を返す命令とします。仮です。
luaに置いては基本バイナリは文字列で扱います。でもnilがあるとそこで止まっちゃうので「lua_pushlstring」でnilを許容した文字列をluaに送り込みます。という点が特殊なので作成の際は注意してください。
話が脱線すると、このあたりのバイナリデータの取り扱いの仕様もあるので、luaの標準文字コードは5.3でutf8でほぼ決定なのかなと思います。無理にutf16にしなくてもね。文字列やりとりする際は変換が必要なんですが、utf16とutf8はかなり高速に相互変換出来るのでまぁいいかなーって思います。
で、これをloaderにすり替えます。

if _VERSION=="Lua 5.1" then
	package.preload[1]=onmemoryloader
end
if _VERSION=="Lua 5.2" or _VERSION=="Lua 5.3" then
	package.searchers[1]=onmemoryloader
end

一応バージョンによって名前が違うので、両対応しておきましょう。
とりあえず一番最初のサーチに入れないと、普通にlua標準ファイル読み込みしちゃうので、一番最初に入れます。
この場所が最良なのかどうか、と言う点は私はわかりませんので公式に聞いてください。

githubにおいておきます


https://github.com/Ko-Ta2142/lua_memoryload
まだgithub使い慣れてませんが適当に組んだものを置いておきます。zlibでお使いください。
使い方はこれをrequireすれば、それ以降のrequireで作用します。
最小限動作させるには「memoryload.onload」にロードしてバイナリ文字列を返す任意のイベント(function)をセットする必要があります。

弊害と利点

この方法ではDLLの読み込みは出来ません。やったら怒られました。なので、luasocketなどのモジュールと併用する際は気をつけてください。
また、moonscriptのようにloaderを乗っ取ってスクリプトluaに変換するもの(コンパイル済は大丈夫)とも衝突します。その場合は、デバッグとリリースでloaderの順序を変えたりして回避するのが一番簡単かとおもいます。
利点は簡単なマクロの実装などもありますが、まずはutf8-bomなどの文字コード対応が一番幸せになれるかなって思います。