MaxScriptTip: ハッシュテーブル(辞書・連想配列)を使う

12月 21, 2016 0

ハッシュテーブルとは

ハッシュテーブルとは配列のようにキーを指定してアクセス出来るコレクションの一種で、 言語によっては辞書(Dictionary)や連想配列などと呼ばれています。

配列とは違い、数値以外のデータをキーとして指定する事が出来ます。

-- 配列要素へのアクセスの例
-- キーは1以上の整数のみ
ary[1]

-- ハッシュテーブル要素へのアクセスの例
-- 数値以外をキーに出来る
-- もちろん数値もキーに出来る
hash["key"]

残念ながらMaxScriptにはハッシュテーブルが無く、標準では上記のようなアクセスは出来ないのですが…

また、配列はデータの検索を行うとき、要素の先頭から順次検索する必要があるので、非常に遅くなるという問題があります(findItem等)。しかし、ハッシュテーブルではデータが”順番”を持たないという特性があり、要素の検索が極めて高速に行えます。

例えば以下のようなデータを追加したとします。

hash[1] = "Val1"
hash[2] = "Val2"
hash[3] = "Val3"

これが配列であれば、当然、要素”Val1”,”Val2”,”Val3”はその順番どおりに格納されているのですが、ハッシュテーブでは必ずしもその順番で格納されているとは限りません。

ハッシュテーブでは、キーと要素のペアがランダムに格納されており、キーを指定してアクセスすると、その都度ペアとなる要素を内部から検索するような仕組みになっています。

ではなぜ検索が高速に行えるのかというと・・・これは少し複雑な仕組みになっているのですが、簡単に説明すると、キーの値をハッシュ化(重複のない一意な数値化)し、その数値をソートして保持しています。
そうすることで、ハッシュ値を使ってバイナリサーチ(二分探査)を行う事ができ、常に高速な検索が行えるというわけです。

バイナリサーチでは要素数が増えても、ほとんど検索時間は増えません。
その為、キーを整数にする場合でも、膨大なデータから要素を頻繁に検索する必要がある時は
非常に有効です。

MaxScriptでハッシュテーブルを利用する

それでは、実際にMaxScriptでハッシュテーブルを利用する方法を紹介します。
例によって.Netを使用します。

-- ハッシュテーブルの初期化
ht = dotNetObject "System.Collections.Hashtable"

-- 要素の追加
ht.add 1 "val1"  -- 1 がkey, "val1"が値
ht.add 2 "val2"
ht.add "text1" "val3"  -- "text1"がkey, "val3"が値

-- 要素の設定
ht.set_item 1 "val4"
ht.set_item "text2" "val5"

-- 要素の取得
ht.get_item 1       -- "val4"
ht.get_item 2       -- "val2"
ht.get_item "text1" -- "val3"

-- 要素の取得の別の書き方
ht.Item[1]          -- "val4"
ht.Item["text2"]    -- "val5"

-- 存在しないキーはundefinedを返す
ht.get_item 3       -- undefined
ht.Item["text3"]    -- undefined

add, set_itemでは1つ目の引数がキー、2つ目が値になります。
get_itemではキーを指定して値を取得しています。

また、データの取得ではItem[]といった書き方も出来ますが、設定では使えないようです。
set_itemと合わせた時非対称になるので、私はよくget_itemの方を使っています。
オリジナルの.NETの方では存在しないキーにアクセスした時エラーが発生しますが、MaxScriptから使用する時はundefinedを返すようです。

注意点として、addとset_itemは良く似ているように見えますが、既に存在するキーを再度指定した場合、addではエラーが発生するのに対し、set_itemでは値の置き換えが行われます。

全ての要素に順次アクセスする

-- 値の追加
ht = dotNetObject "System.Collections.Hashtable"
ht.set_item 1 "val1"
ht.set_item 2 "val2"
ht.set_item 3 "val3"

-- 順次アクセス
enum = ht.GetEnumerator()
while enum.MoveNext() do
    format "%: %\n" enum.Key enum.Value

-- 出力:
-- 3: val3
-- 2: val2
-- 1: val1

for文を使ってアクセス出来ればいいのですが、残念ながら対応していないようです。
代わりに、getEnumerator関数を使って順次アクセスオブジェクトを取得し、MoveNext関数で全ての要素を取得する事が出来ます。
MoveNextは取得できる要素がこれ以上無くなったときfalseを返すので、自動的にwhile文を抜けます。

ここで、出力の順序がインデックス順にはなっていない事が確認出来ます。

その他の機能

-- 要素数プロパティ
ht.Count

-- 指定した要素を削除
ht.Remove key

-- 全ての要素を削除
ht.Clear()

-- 要素が含まれているか確認する
ht.Contains key      -- 指定したキーが存在すればtrue
ht.ContainsKey key   -- Containsの別名
ht.ContainsValue val -- 指定した値が存在すればtrue

.NETで扱えないオブジェクトを使う

残念ながらMaxScript上の一部のオブジェクトは.NETでは使用できません。
特にノードやマテリアル等のシーンオブジェクトは、頻繁に使う上に代えが効きません。

ではそういったオブジェクトをハッシュテーブル上で使う方法が全く無いのかというと、そういうわけでもありません。

方法として
1. 実データは配列で管理し、配列のインデックスをハッシュに格納する
2. オブジェクトを一意に特定するハンドルを利用する
等があります。

2.の一意に特定するハンドルについては、ノードオブジェクトなら.handleプロパティ、それ以外のオブジェクト(モディファイヤ、コントローラ、マテリアル等)ではAnimHandleを利用できます。
どちらの場合でも、オブジェクトを一意に特定する整数値を取得する事ができます。

以下は1と2の両方を組み合わせて利用した例です。選択したオブジェクトをマテリアル毎にグループ化しています。

ht = dotNetObject "System.Collections.Hashtable"
nodes = #()

-- キーをマテリアルのハンドル、
-- 値をnodesのインデックスとしてハッシュテーブルを構築する
for sel in selection do
(
    mtl = sel.material
    mtlHandle = if mtl != undefined then GetHandleByAnim mtl else 0P

    if ht.Contains mtlHandle then
    (
        nodeIdx = ht.get_item mtlHandle
        append nodes[nodeIdx] sel
    )
    else
    (
        append nodes #(sel)
        ht.set_item mtlHandle nodes.count
    )
)

-- マテリアルスロット1のマテリアルが割り当てられた
-- オブジェクトがあれば、その一覧を取得する
mtlHandle = GetHandleByAnim meditMaterials[1]
if ht.Contains mtlHandle do
(
    nodeIdx = ht.get_item mtlHandle
    mtlObjs = nodes[nodeIdx]
    -- 例として、mtlObjsは #($box001, $box002, ...) のような配列
)

-- マテリアルが割り当てられてないオブジェクトの一覧
if ht.Contains 0P do
(
    nodeIdx = ht.get_item 0P
    mtlObjs = nodes[nodeIdx]
)

注意点として、AnimHandleはそのプロセス内でしか一意性を保たないようになっている為、3dsMaxを再起動したりした場合はハンドルの値が変わるという事があります。
よって、ハンドルをファイルに保存するような使い方には適していません。

対してノードの.handleプロパティでは、同一のシーンオブジェクトであればセッション外であっても同一の値を保持するようです。

パフォーマンスについて補足

これはハッシュテーブルに限った話ではないのですが、何度も呼び出す関数は予めローカル変数に取り出しておく事で、関数呼び出しのパフォーマンスを向上させる事が出来ます。

ht = dotNetObject "System.Collections.Hashtable"

setItem = ht.set_item
for i = 1 to 100 do
    setItem i "value"

enum = ht.GetEnumerator()
moveNext = enum.MoveNext
while moveNext() do
    format "%: %\n" enum.Key enum.Value

MaxScriptTip: 特定のマテリアル下にある全てのサブ要素を取得する

9月 23, 2016 0
今回はマテリアルのサブ要素(マテリアル、テクスチャ)の取得について。

マテリアルは要素の下に別の要素がツリー状に接続される、いわゆる階層構造を持っています。
こういった構造では、再帰処理による取得が強力です。

以下は指定マテリアル下の全てのサブ要素を取得する関数です。
fn getAllSubMtlAndMaps mtl subs:#() =
(
    if mtl != undefined and (appendIfUnique subs mtl) do
    (
        if (superclassof mtl) == material do
        (
            for i = 1 to (getNumSubMtls mtl) do
                getAllSubMtlAndMaps (getSubMtl mtl i) subs:subs
        )
  
        for i = 1 to (getNumSubTexmaps mtl) do
            getAllSubMtlAndMaps (getSubTexmap mtl i) subs:subs
    )
    subs
)
引数のmtlにはマテリアルオブジェクトを指定してください。 例えば・・・
getAllSubMtlAndMaps $.material -- 選択オブジェクトのマテリアル

getAllSubMtlAndMaps meditMaterials[1] -- マテリアルスロット1のマテリアル
といった感じです。

オプション引数のsubsは内部使用専用なので、基本的には何も指定しないでください。

内部的な処理を簡単に説明すると、マテリアルのサブマテリアルとサブテクスチャを全て検索し、それらの要素に対してgetAllSubMtlAndMapsを再帰的に実行しています。
ただし、この時サブ要素がループ接続されている可能性を考慮し(実際にそういう状況になるのかは知りません…)、appendIfUniqueで要素がまだ追加されていない時だけ再帰しています。
よって、subsに最初から要素を追加しておいた場合、そのマテリアル以下は検索されない事になります。

ただし、取得されるサブ要素は、マテリアルとテクスチャマップが混ざってしまっているので、個別に必要な場合は自力で分類する必要があります。
local subs = getAllSubMtlAndMaps $.material
local mtls = #()
local maps = #()
for s in subs do
(
    if (superclassof s) == material then
        append mtls s
    else
        append maps s
)
こんな感じです。

余談ですが、自分が初めて再帰関数を使ったとき、再帰内でループする事に気付かず、処理がフリーズする原因を見つけられずに3日くらい悩んだ思い出があります。
再帰処理は非常に強力ではありますが、度々バグの原因になったりして扱いの難しさを時々実感します。

MaxScript配布: TR Camera Plane

9月 04, 2016 0
TR Camera Planeは、3dsMax上で簡単にイメージプレーンを配置出来る、プリミティブタイプのプラグインです。

カメラから任意の距離で、カメラに連動して動くイメージプレーンを配置する事が出来ます。 また、カメラ前に配置しない場合でも、より簡単にセットアップ出来るイメージプレーンとしても使用できます。

類似のスクリプトは幾つかありますが、シンプルさと使い勝手を重視しています。


動画・スクリーンショット





機能・特徴

  • カメラを指定して連動させる事が出来ます。
  • カメラの視野角によって自動的にプレーンサイズを調整。
  • 平行投影に対応。
  • レンダー解像度を変更すると自動的にサイズを調整。
  • イメージを指定すると、自動的にWidth, Heightを設定します。
  • "Drop image here"に画像をドラッグ&ドロップするだけで読み込み可。
  • カメラを設定していない時でも、単純なイメージプレーンとして使用可能。
  • プレーンの透明度をロールアウト上から調整可能。
等々...

Download

TR Camera Plane 最新版

インストール

ダウンロードした.zip内にあるReadmeに従ってください。
インストール方法はMZPパッケージか手動インストールから選択出来ます。

MaxScriptTip: クリップボード操作が遅いという話

8月 15, 2016 0
MaxScriptでクリップボードを操作するには、クリップボードにテキストを送るsetClipboardTextとテキストを取得するgetClipboardTextの2つの関数を使用できます。

しかしこの2つの関数、使ってみると異常に遅いです。

どうしてこんなに遅いのか分からないのですが、数千文字送るだけで数秒程かかります。(面倒なので正確な速度計測はしていませんが…)

流石にその速度だと実用性に難があったので、色々試してみたのですが、最終的にはいつもの様に.NETを使う事にしました。
.NET万歳。
local ss = StringStream ""

-- 文字列をStringStreamに書き込み

local clipboardCls = dotNetClass "System.Windows.Forms.Clipboard"
clipboardCls.SetText (ss as string)
free ss
StringStreamを使って文字列を高速に結合した後、.NETのClipboardクラスのsetTextで転送しています。

もちろん転送データが最初から文字列な場合はそのまま転送すればいいのですが、まぁ普通はデータを整形する工程があると思うので、このタイミングで行ってしまいます。

StringStreamへの書き込みはappendを使うか、formatでtoオプションを指定します。
append ss "The text"
format "Val: %\n" theValue to:ss
これで文字列を高速にクリップボードに送る事が出来ます。
逆に文字列を取得する時は以下のようにします。
local clipboardCls = dotNetClass "System.Windows.Forms.Clipboard"
local theText = clipboardCls.GetText()
まぁWindowsアプリ用に作られた.NETですから、こういった処理はネイティブのWindowsアプリとほぼ同等の速度が出せます。

MaxScriptTip: 座標系の変換

7月 18, 2016 0
MaxScriptでは座標系の制御にin coordsysコンテキストが使えますが、場合によってはこのコンテキストが作用しないケースがあります。

例えば法線編集モディファイヤで扱う法線情報は、常にローカル座標としてやり取りされます。
また、各モディファイヤのギズモ関係も多くの場合in coordsysが効きません。
しかしそうなってくると困るのが、異なるオブジェクト/モディファイヤ間での座標のやり取りです。
そうういったケースでは、座標を自力で変換する必要があります。

ローカル→ワールド

特定のオブジェクトのローカル座標をワールド座標に変換するには、対象オブジェクトの行列を取得して、ローカル座標に行列変換をかけます。
local mtx = in coordsys #world obj.transform
local worldVerts = (for v in localVerts collect v * mtx)
座標(point3)への行列変換は、単に行列(matrix3)を掛けるだけです。


ワールド→ローカル

逆にワールド座標系をローカルに戻す場合は、対象オブジェクトの逆行列を取得します。
逆行列とは読んで字の如く、特定の行列と全く逆の変換をする行列です。
逆行列は、行列オブジェクトに対してinverse関数を使う事で取得出来ます。
local invMtx = in coordsys #world inverse obj.transform
local localVerts = (for v in worldVerts collect v * invMtx)
分かってしまえば簡単ですね。


ローカル→ローカル

最後に異なるローカル間での変換について。
上の2つの例では行列の取得を#worldコンテキストで行っていますが、ローカル間で直接変換する場合は変換先オブジェクトの座標コンテキストで取得します。
local mtx = in coordsys targetObj selfObj.transform
local targetVerts = (for v in selfVerts collect v * mtx)
もちろんワールド座標を経由して変換しても同じ座標を取得する事が出来ますが、変換回数が1回増えるのでどちらがいいかはケースバイケースですね。

(補足)法線変換の注意点

変換対象が座標の時はいいのですが、対象が法線のような方向ベクトルの時、注意点があります。
法線には移動オフセットの概念が無いので、行列に移動変換が入っていると正しい計算結果になりません。
そういう訳で、その場合は行列の移動変換をクリアしてやる必要があります。

それともう一つ、変換にスケールが入っていると変換後の法線が単位ベクトル(長さ1.0のベクトル)にならない事があります。 これについても変換後に正規化する事で対応する事ができます。
local mtx = in coordsys #world obj.transform
mtx.row4 = [0,0,0]
local newNormals = (for n in oldNormals collect normalize (n * mtx))

MaxScriptTip: 指定ディレクトリをエクスプローラで開く

5月 29, 2016 0
ディレクトリ(フォルダ)をエクスプローラで開くには、リファレンスにある通りShellLaunchを使用します。 DOSCommandを利用しても同じ事が出来ますが、DOSウィンドウを開いたりしたくないのでこちらを利用します。

今回はせっかくなので、指定したファイルを選択状態にして開くオプションも付けてみます。
-- エクスプローラで対象を開く
-- thePath  select
-- File   true  : 指定したファイルを選択状態でディレクトリを開く。
-- File   false : 指定ファイルを関連付けられたプログラムで開く。
-- Directory  true  : 指定ディレクトリを選択状態で1つ上のディレクトリを開く。
-- Directory  false : 指定ディレクトリそのものを無選択状態で開く。
fn openWithExploer thePath select:true =
(
 thePath = pathConfig.normalizePath thePath
 if select then
  ShellLaunch "explorer.exe" (stringFormat "/e,/select,\"{0}\"" thePath)
 else
  ShellLaunch "explorer.exe" (stringFormat "/e,\"{0}\"" thePath)
)
使ってみる。
-- notepad.exeを選択した状態でWindowsフォルダを開く
openWithExploer @"C:/Windows/notepad.exe" select:true
-- Autodeskフォルダを選択した状態でProgram Filesフォルダを開く
openWithExploer @"C:/Program Files/Autodesk" select:true
-- 単にAutodeskフォルダを開く
openWithExploer @"C:/Program Files/Autodesk" select:false
どうでしょうか?

ちなみに、select:false状態でファイル等を指定すると、関連付けられたプログラムで単にそのファイルが開かれます。
select:trueでディレクトリを指定すると、その1つ上のフォルダが開かれるのもミソですね。

今回は短めですが、こんな感じで。

MaxScriptTip: 頂点を特定平面上に投影(整列)する

5月 05, 2016 0
今回は平面への投影処理について。 

通常、特定の軸(XYZ)平面上への投影は、単に頂点の値を揃えるだけで出来ます。
例えば、全ての頂点をYZ平面上に投影したければ、X座標を0.0に揃えるだけで整列できます。

しかし、これが単純な軸平面上ではない場合、もしくは任意の移動方向に制限して投影する場合、一気に話が難しくなります。

以下は頂点座標を、任意の平面上に任意の方向から投影する関数です。
PROJ_THRESHOLD = 0.0001  -- 1

fn projToPlane plnNrm plnOfs vert vec =
(
    local vecAng = dot plnNrm vec  -- 2

    if (abs vecAng) > PROJ_THRESHOLD then  -- 3
    (
        local closestDist = dot plnNrm (plnOfs - vert)  -- 4
        local dist = closestDist / vecAng  -- 5
        vec * dist + vert  -- 6
    )
    else
        vert  -- 7
)

引数説明

plnNrm
平面の法線です。予め正規化しておく必要があります。
法線というのは面から垂直に伸びている単位ベクトルの事で、例えばYZ平面であればZ軸の方向を向いているので[1,0,0]になります。
正規化するにはnormalize関数を使用します。
plnOfs
平面のオフセット値です。平面上にある一点の座標を指定します。
この一点は平面上に有りさえすれば、何処を指定していても問題ありません。
vert
投影前の頂点座標です。
vec
投影を行う方向ベクトルです。
このベクトルも正規化されている必要があります。
またこのベクトルが面と完全に平行だった場合、処理はキャンセルされます。(後述)


処理内容説明

1. PROJ_THRESHOLD = 0.0001
面とvecが平行だった時、どの程度までなら処理するかの許容値を設定しています。
1.0を完全に垂直、0.0を完全に平行とし、0.0以外の値を設定します。 イメージしてみると分かりますが、面と移動ベクトルが完全に平行だと、頂点は面にぶつからず処理不能となります。
2. local vecAng = dot plnNrm vec
plnNrmとvecがどの程度同じ向きを向いているかを計算しています。
全く同じ向きなら1.0、正反対を向いていれば-1.0、直角なら0.0を返します。
いわゆる内積演算です。
3. if (abs vecAng) > PROJ_THRESHOLD then
2の計算結果の絶対値がPROJ_THRESHOLDより小さければ、処理をキャンセルしています。
4. local closestDist = dot plnNrm (plnOfs - vert)
頂点と平面との再接近距離を計算しています。
具体的にはplnOfsからvertへ向かうベクトルを計算し、plnNrmと内積を取っています。
こうすることで面と頂点との距離を計算しています。不思議ですね。
5. local dist = closestDist / vecAng
再接近距離をvecAngで割る事で、vec方向に移動した時の衝突距離を計算しています。
そもそもvecAngは移動ベクトルと面法線の一致度である為、この2つが違えば違う程、小さな値(0に近い値)になっていきます。 結果、distはより大きな値になります。
ただし0.0の時(完全に平行な時)は結果が無限大になってしまう為、処理不可です。
6. vec * dist + vert
最終的な移動先座標を計算しています。
単位ベクトルであるvecと移動距離distを掛ける事によって、移動オフセットを計算する事が出来ます。
7. vert
vecが面と完全に平行だった時、引数vertをそのまま返しています。
ここは、必要に応じて例外処理等に変更しても良いと思います。

実際に使ってみます。
thePoly = selection[1]
plnNrm = normalize [0.5, 0, 1]
plnOfs = [0, 0, 10]
vec = z_axis

in coordsys #world
(
    for vi = 1 to (polyop.getNumVerts thePoly) do
    (
        local v = polyop.getVert thePoly vi
        polyop.setVert thePoly vi (projToPlane plnNrm plnOfs v vec)
    )
)
面の向きはXに傾いたZ方向、面のオフセットはZ+に10ずらしています。
また、移動ベクトルはZ軸に指定しています。



どうでしょうか?
傾いた面上に整列しているのが分かるかと思います。
また面は原点を通っておらず、Z方向にオフセットされている事も確認出来ます。



本来であればもっと画像を多用して解説すべき記事ですが、管理人の体力が限界なのでこの辺で。

MaxScriptTip: 文字列の整形とDotNet

4月 18, 2016 0
MaxScriptで最も簡単な文字列整形方法は、加算演算(+)を使った結合かと思います。
i = 25
s = "Test1"
res = "Values: (" + s + ", " + (i as string) + ")"
>> "Values: (Test1, 25)"
しかし見ての通り、簡単な整形でさえ長くなりがちな上、読みづらくなってしまいます。

そこで次によく使われる方法として、formatがあります。
formatはtoオプションを指定する事によって、出力先をStringStreamに変更する事が出来ます。
s = "Test2"
i = 36
ss = StringStream ""
format "Values: (%, %)" s i to:ss
res = (ss as string)
>> "Values: (Test2, 36)"
こちらはパターン文字列を使って文字列整形出来るので、整形自体は楽ですが、StringStreamを用意する必要がある為コードは長くなってしまいます。
それでも整形が複雑になればなるほど、こちらの方がすっきり綺麗に書く事が出来ます。


最後に、最もオススメな方法として、.NET FrameworkのString.Formatを使用する方法を紹介します。
s = "Test3"
i = 48
res = (dotNetClass "System.String").Format "Values: ({0}, {1})" s i
>> "Values: (Test3, 48)"
非常にすっきり収まります。
必要なら、Stringクラスオブジェクトを予め作っておいて、使い回してもいいでしょう。

書式の仕様として、formatのような % 指定ではなく、{0}, {1}といった大カッコ+引数番号で指定し、必要なら番号のあとに:を付けて、追加の書式指定子を指定する事も出来ます。

以下の例では、浮動小数の少数桁数を4桁に指定して出力しています。
strCls = dotNetClass "System.String"
for i = 1 to 5 do
    print (strCls.Format "{0:F4}" (random 0.0 1.0))
>> "0.4916"
>> "0.3504"
>> "0.2075"
>> "0.9754"
>> "0.7172"
また、埋め込み引数の数が3つを超える場合、配列を使って指定する事が出来ます。
strCls = dotNetClass "System.String"

-- 3つまではOK
strCls.Format "{0}, {1}, {2}" "Foo" "Bar" "Baz"
>> "Foo, Bar, Baz"

-- 3つ以上は配列で指定する
strCls.Format "{0}, {1}, {2}, {3}, {4}" (for i = 1 to 5 collect i)
>> "1, 2, 3, 4, 5"
多機能な分少々複雑ではありますが、使いこなせば非常に強力なツールなので、是非使っていきたい機能です。

細かい仕様に関しては.NETのリファレンスか、詳しく解説しているサイト様も沢山ありますので、そちらで調べてみると良いかと思います。
以下に使えそうなページを幾つか紹介しておきます。

リファレンス: 標準の数値書式指定文字列
リファレンス: カスタム数値書式指定文字列
smdn様: 書式指定子
@IT様:  数値を右詰めや0埋めで文字列化するには?

MaxScriptTip: マテリアルのサムネイルBitmapを取得する

3月 26, 2016 0
マテリアルのサムネイルBitmap取得について。
標準のMaxScriptではこのような機能は無いようなので、DotNet用のMaxAPIを叩いて作成します。
fn createMtlThumb mtl size:#large =
(
    -- MaxAPIインターフェース作成
    local iGlobal = (dotnetClass "Autodesk.Max.GlobalInterface").Instance
    if iGlobal == undefined do
        return undefined
    
    -- マテリアルを.NET APIマテリアルに変換
    local mtlHandle = dotNetObject "System.UIntPtr" (GetHandleByAnim mtl)
    local iMtl = iGlobal.Animatable.GetAnimByHandle mtlHandle
    if iMtl == undefined do
        return undefined

    -- サムネイルを作成
    local ps = dotnetclass "Autodesk.Max.PostageStampSize"
    local pSize = case size of (#large: ps.Large; #small: ps.Small; #tiny: ps.Tiny)
    local pStamp = iMtl.CreatePStamp pSize true
    
    -- サムネイルをBitmapにコピー
    local bytes = pStamp.Image
    local width = pStamp.Width
    local dstBmp = bitmap width width
    local step = width * 3
    
    for y = 1 to bytes.count by step do
    (
        row = (for x = y to (y + step - 1) by 3 collect [bytes[x + 2], bytes[x + 1], bytes[x]])
        setpixels dstBmp [0, width -= 1] row
    )
    
    -- 開放
    pStamp.Dispose()
    iMtl.Dispose()
    
    return dstBmp
)

thumbnail = createMtlThumb $.material size:#large
display thumbnail
第1引数にはMaterialオブジェクトを、第2引数には#large, #small, #tinyを指定できます。
それぞれのサイズは以下のようになります。
#large: 88px
#small: 32px
#tiny: 24px

戻り値はbitmapオブジェクトです。

上記スクリプトを実行すると選択しているオブジェクトのマテリアルがレンダリングされて、以下のようにフレームバッファに表示されます。

勿論、フレームバッファへの表示はテスト用なので表示する必要はありません。

CreatePStampの第二引数をfalseにするとレンダリングせずに取得する(?)ようですが、キャッシュが無かった時にどうなるかは試してません。
現状ではレンダリングにかかる時間も微々たるものですので、普通にレンダリングする仕様でいいかと思います。


最後に

今回の記事作成にあたり、こちらのスレッドを参考にさせて頂きました。
http://forums.cgsociety.org/archive/index.php?t-1239911.html
ありがとう海外のエロ偉い人。

MaxScriptTip: Maxオブジェクトを一意に特定する

3月 26, 2016 0
MaxはMaya等と違って、オブジェクト名がシーン内で一意であることを保証していません。

当然全くの同名(同パス)オブジェクトが複数シーン内に存在する事になり、オブジェクト名では特定のオブジェクトを必ずしもハンドルする事が出来ません。
MaxScript内で常にオブジェクトのインスタンスを保持出来れば良いのですが、それが出来ないケースもあります。
例えば、.NETと連携させる時やクリップボードに転送する時等…。

そこで今回は、MaxScript内で、インスタンス保持以外の方法で、オブジェクトをハンドルする方法を2つ紹介します。


inode.handleを使う方法

ノードオブジェクト(シーン内に座標系を持って存在しているオブジェクト)はinodeインタフェースのhandleプロパティが使えます。
local handle = $box001.inode.handle
これにより、シーン内のノードを一意に特定出来るハンドルを取得出来ます。
ハンドルからノードを取得するには以下のようにします。
local node = maxOps.getNodeByHandle handle
リファレンスによると、$box001.handleとするよりは$box001.inode.handle等とする方が良いらしいです。
inodeを省略した場合、元々「handle」プロパティを持っているオブジェクト(Teapot等)はうまくハンドルを取得出来ない可能性があるからだそうです。


AnimHandleを使う方法

ノード以外のオブジェクト(マテリアル、モディファイヤ、コントローラ等)をハンドルするにはAnimHandleという仕組みが使えます。
AnimHandleは、文字通りAnimatableオブジェクトに設定出来るハンドルシステムで、シーン内のほぼ全てのオブジェクトのハンドルを取得する事が出来ます。
local mtlHandle = GetHandleByAnim $box001.material
ハンドルからマテリアルを取得するには以下のようにします。
local mtl = GetAnimByHandle mtlHandle
この方法は、うまく使うと.NET APIにオブジェクトを転送する時等にも使えます。
(.NET API内でもGetAnimByHandleは使える為)

ただし、こちらのハンドルはMaxのセッションが終了するとリセットされてしまうみたいなので、外部ファイルに保存しておいて使うような使い方には向きませんね。

どちらを使うかはケースバイケースだと思いますが、知っておくと便利かもしれません。

MaxScriptTip: 簡単なMZPインストーラ作成

3月 06, 2016 0
Maxに簡単にスクリプトをインストール出来る、MZPパッケージの作成方法について。

MZPは一度作ってしまうと何度も使い回せて便利なのですが、最初は何となく取っ付きづらい印象があると思います。
そこで、今回は必要最低限でのMZPパッケージの作り方を説明します。

今回作成するMZPパッケージは以下のファイルで構成されています。

  • マクロファイル (sample.mcr)
  • 暗号化済みスクリプトファイル (sample.mse)
  • mzp.run
  • インストーラスクリプト (install.ms)

マクロファイルとスクリプトファイルをそれぞれ分離したのは、マクロファイルを暗号化出来ないというMaxの仕様の為です。(実際には作れますが、動作しません。)
スクリプトそのものはsample.msに書き、暗号化後、それをマクロから呼び出すというフローを想定しています。

最終的に、上記4つのファイルをZIP形式で圧縮することで、パッケージを作成します。

mzp.run

name "SampleTool"
version 1.00

copy "sample.mcr" to "$userMacros/SampleTool"
copy "sample.mse" to "$userScripts/SampleTool"

run install.ms
drop install.ms

clear temp
mzp.runはMZPパッケージがMaxにドロップされた時、最初に実行されるコマンドファイルです。
ただしコマンドとは言っても、MaxScriptを直接使用するわけではなく、mzp.run専用コマンドを使用します。
各コマンドについて簡単に説明します。

name
ツール名を指定します。
version
ツールバージョンを指定します。必ずしも浮動小数である必要はありません。
copy
パッケージ内のファイルを指定したフォルダにコピー(配置)します。
指定出来るフォルダ名はリファレンスを参照してください。
記号パス名
注意点として、Program Files下のMaxインストールフォルダへファイルをコピーする時、Maxに管理者権限(もしくはフォルダアクセス権)が無いとコピーに失敗します。
その場合、同名の$user~フォルダにインストールする等して回避する必要があります。
また、コピー先にフォルダが無かった場合、自動的に作成されます。
run
.mzpがドラッグ&ドロップ以外の方法で実行された時、指定したスクリプトを実行します。
このコマンドはrun.mzp内で複数回実行する事が出来ます。
drop
.mzpがMaxにドロップされた時実行されます。ただし、.mzpがドロップ以外の方法で実行された時は実行されません。
また、このコマンドはmzp.runにつき1回しか実行出来ません。
clear
.mzpパッケージが一時的に展開されたtempフォルダを削除します。

run, dropを別々に指定しているのは、.mzpの実行のされ方でどちらが呼び出されるかが代わるからです。runは複数回実行出来ますが、dropは出来ないので、基本はinstall.msを呼び出すだけにしておいて、細かい処理はinstall.ms内でやるようにした方がいいかもしれません。

その他いろいろコマンドはありますが、全て説明しているとキリが無いので、詳細はリファレンスを参照してください。
Zip ファイル スクリプト パッケージ

install.ms

(
    local msgTitle = "SampleTool Installer"
    local mcrPath = @"$userMacros/SampleTool/sample.mcr"
    
    if doesFileExist mcrPath then
    (
        fileIn mcrPath
        local msg = "Installation Completed.\n" + \
                    "---------------------------\n" + \
                    "Category: SampleCategory\n" + \
                    "MacroName: SampleTool"
        messageBox msg title:msgTitle beep:false
    )
    else
        messageBox "MacroScript not found." title:msgTitle beep:true
)
mzp.runから呼び出されるインストールスクリプトです。
絶対に必要というわけではないですが、今回はインストール結果の表示の為に使用しています。
インストール先のカテゴリ等表示してあげると親切かもしれません。

インストール後にfileInでマクロをロードしています。
こうすることでMaxの再起動なしに、すぐにマクロを使えるようになります。

sample.mcr

macroScript SampleTool
category:"SampleCategory"
(
    on execute do
    (
        try
            fileIn @"$userScripts/SampleTool/sample.mse"
        catch
            messageBox "Script not found.\nsample.mse" title:"SampleTool"
    )
)
暗号化済みスクリプトを呼び出す為のマクロファイルです。
スクリプトファイルが存在すればfileInで実行し、無ければメッセージボックスを表示します。

sample.mse

暗号化したスクリプトファイルです。
暗号化したスクリプトは単体でツールバー等に登録できないという致命的な問題が有る為、ほぼマクロスクリプトとペアでリリースするのが基本になります。
マクロスクリプトが必要ないケースととしては、スクリプトプラグイン等、startupScriptsに配置してその後呼び出さないスクリプト等があります。


パッケージ作成

最後に作成した4つのファイルを全て選択し、ZIPファイルとして圧縮します。
ここで重要なのはファイルを入れているフォルダではなく、ファイルそのものを選択して圧縮する必要があるという事です。
最後にZIPファイルの拡張子を.mzpに変更して完成です。
Maxにドロップして、無事インストール完了ダイアログが表示されるか、マクロは実行出来るかを確かめてみてください。

MaxScript配布: 法線合成スクリプトTR_BlendNormals

3月 04, 2016 0
法線合成スクリプト、TR_BlendNormalsを公開します。

このスクリプトは3D空間上で法線をサンプリングし、オブジェクトから他のオブジェクトへ法線を転送or合成する事が出来ます。

 割りとよくあるタイプのスクリプトですが、煩わしい設定なしに手軽に利用出来るのが利点です。

また、転送元、転送先ともにポリゴンやメッシュである必要は無く、転送先に「法線編集」モディファイヤさえ適用出来れば動作します。

その他細かい仕様は以下のようになります。

  • 転送先オブジェクトに、既に法線編集モディファイヤが有るか検索します。
    有った場合、そのモディファイヤを転送ターゲットにします。
    (ターゲットにしたくない場合、モディファイヤを非表示にしておいてください。)
    無かった場合、自動的に法線編集モディファイヤを追加します。
  • 転送元オブジェクトは内部的に、一旦Meshへと変換されます。
    よって、Meshに変換可能なオブジェクトであれば、全て転送元に使用できます。
  • 法線のサンプリングは、3Dグローバル空間上で行われます。
    よって、2つのモデルが同一のメッシュ構造をしている必要はありません。
  • 合成率は%で指定する事ができます。
    0%は何もせず、100%は合成せずそのまま転送します。
    1~99%の間では2つのオブジェクト法線が合成されます。

スクリーンショット







ティーポットの形状を維持したまま、影だけを球体からコピーする事が出来ます。
また、板ポリを重ねて作った木の葉に、ボリューム感を持たせるといった使い方も出来ます。

インストール

ダウンロードした.mzpファイルを3dsMaxのビューポート上にドラッグ&ドロップしてください。
Category: TR ToolsにマクロTR_BlendNormalsが追加されます。
3dsMax 2014及び2015日本語版で動作テストしています。

ダウンロード

TR_BlendNormals Ver1.00

TR_ShellSwitch 配布

2月 23, 2016 0

TR_ShellSwitchは最大10個までのマテリアルを接続出来るよう拡張した、シェルマテリアルです。

マルチマテリアルのようにマテリアル毎にラベルを付けられる他、ビューポート、レンダリング用マテリアルを個別に切り替える事が出来ます。
また、シーン内のShellSwitchを一括で切り替える事ができる、グローバススイッチャーを搭載しています。

スクリーンショット

ダウンロード

Download (Ver 1.11)
Old Versions

更新履歴

16/02/23 (ver1.00)
ファーストリリース。
16/03/01 (ver1.01)
インストール先を$startupScriptsから$userStartupScriptsに変更。
($startupScriptsでは管理者権限が必要だった為)
16/11/13 (ver1.02)
UI言語を英語に変更、及び英語版Maxでの文字化け解消。
16/11/13 (ver1.10)
グローバルスイッチャーの搭載。
16/12/07 (ver1.11)
グローバルスイッチャーが起動出来ない問題を解消。
その他軽微な修正。

インストール

ダウンロードしたmzpパッケージを3dsMax上にドラッグ&ドロップすると自動的にインストールが実行されます。
もしくは、必要に応じてmseファイルを手動でコピーしてインストールする事も出来ます。
詳しくはReadme参照。

その他

現状では10個までのマテリアル接続に対応しています。
本来なら可変長にしたかったのですが、テスト時にSME上でどうしてもスロットに反映されず・・・。
どなたか、スレートマテリアルエディタ上で、マテリアルスロットを可変長に対応させる方法を教えていただけると幸いです。(通常のプラグインではなく、ScriptedPluginに限る。)

MaxScriptTip: SubRolloutに合わせて自動的にサイズを変えるロールアウト

2月 21, 2016 0
今回は前回の記事に続いてRolloutについての記事です。

UIオブジェクトの一つに、「SubRollout」というコントロールがありますが、今回はこのSubRolloutに合わせて動的にサイズを変更するロールアウトを作りたいと思います。


サンプルロールアウト

起動直後。


ボタンスイッチを押すとそれに対応するサブロールアウトが追加されます。
親ロールアウトは、自動的に子ロールアウトのサイズに合わせます。


全てのサブロールアウトを表示した状態。


ロールアウトを畳んだ時も自動でサイズを変更します。



サブロールアウト作成

初めに、子ロールアウトを作成します。
rollout rltSub1 "Sub1" width:190 height:32 category:1  --- 1. カテゴリの指定
(
    local resizeCallback  -- 2. コールバック変数

    checkbox chkOpt "Option" pos:[8,8] width:152 height:16
    
    -- 3. サイズ変更イベント
    on rltSub1 rolledUp isOpen do
    (
        if resizeCallback != undefined do
            resizeCallback()
    )
)

rollout rltSub2 "Sub2" width:192 height:80 category:2
(
    local resizeCallback
    radiobuttons rdoItems "Items" pos:[8,8] width:96 height:36 \
        labels:#("Item1", "Item2", "Item3")
        
    on rltSub2 rolledUp isOpen do
    (
        if resizeCallback != undefined do
            resizeCallback()
    )
)

rollout rltSub3 "Sub3" width:192 height:112 category:3
(
    local resizeCallback
    editText edtMsg "" pos:[4,8] width:180 height:96
    
    on rltSub3 rolledUp isOpen do
    (
        if resizeCallback != undefined do
            resizeCallback()
    )
)
3つのロールアウトを定義していますが、基本的に3つとも内容は同じです。
要点だけ説明します。
  1. カテゴリの指定
    ロールアウト作成時にcategoryパラメータを設定します。
    こうすることで、サブロールアウトが追加された時に自動的に順番を整列してくれるようになります。例えば、category:3のロールアウトを追加した後からcategory:1のロールアウトを追加した場合でも、category:1が先に来るようになります。
  2. コールバック変数
    この変数は親ロールアウトのresize関数を格納するための変数です。
    サブロールアウト作成時に、親からresize関数オブジェクトを受け取って格納します。
  3. サイズ変更イベント
    サブロールアウトが畳まれたり開かれたりした時に発生します。
    親のresize関数を実行してサイズを変更させます。
以上です。

コールバックの概念が解ってないと少々難解かもしれませんが、基本的にrolledUpイベントを親に伝搬する以外は、特別な機能は持っていません

親ロールアウト作成

次に親ロールアウトを作成します。
rollout rltMain "Main Rollout" width:200 height:180
(
    local self
    local subRollouts
    local btmControls
    subRollout srFloater "" pos:[-1,40] width:206 height:102
    checkbutton ckbSub1 "Sub1" pos:[10,10] width:60 height:20
    checkbutton ckbSub2 "Sub2" pos:[70,10] width:60 height:20
    checkbutton ckbSub3 "Sub3" pos:[130,10] width:60 height:20
    button btnGet "Get" pos:[10,150] width:90 height:20
    button btnClose "Close" pos:[100,150] width:90 height:20
    
    -- 3. サイズ変更関数
    fn resize =
    (
        local lastHeight = srFloater.height
        local maxHeight = 1000
        local subsHeight = 4
        
        for rlt in srFloater.rollouts do
        (
            if rlt.open then
                subsHeight += (rlt.height + 25)
            else
                subsHeight += 21
        )
        subsHeight = amin subsHeight maxHeight
        srFloater.height = subsHeight
        
        local offsetY = subsHeight - lastHeight
        self.height += offsetY
        for c in btmControls do
            c.pos.y += offsetY
    )
    
    -- 2. サブロールアウト追加・削除関数
    fn addSub rlt =
    (
        addSubRollout srFloater rlt
        rlt.resizeCallback = resize
    )
    
    fn removeSub rlt =
    (
        removeSubRollout srFloater rlt
    )
    
    -- 1. ロールアウト初期化
    on rltMain open  do
    (
        local btmY = srFloater.pos.y + srFloater.height
        self = rltMain
        subRollouts = #(rltSub1, rltSub2, rltSub3)
        btmControls = (for c in self.controls where c.pos.y > btmY collect c)
        resize()
    )
    
    -- 4. サブロールアウトON/OFFボタン変更イベント
    on ckbSub1 changed state do
    (
        if state then
            addSub subRollouts[1]
        else
            removeSub subRollouts[1]
        resize()
    )
    
    on ckbSub2 changed state do
    (
        if state then
            addSub subRollouts[2]
        else
            removeSub subRollouts[2]
        resize()
    )
    
    on ckbSub3 changed state do
    (
        if state then
            addSub subRollouts[3]
        else
            removeSub subRollouts[3]
        resize()
    )
    
    -- 5. パラメータ取得ボタンクリックイベント
    on btnGet pressed do
    (
        local opt = subRollouts[1].chkOpt.checked
        local items = subRollouts[2].rdoItems.state
        local msg = try subRollouts[3].edtMsg.text catch ""
        
        ss = StringStream ""
        format "Option: %\n\n" opt to:ss
        format "Items: %\n\n" items to:ss
        format "Text: '%'" msg to:ss
        
        messageBox (ss as string) title:"Result" beep:false
    )
    
    on btnClose pressed do
    (
        destroyDialog self
    )
)

createDialog rltMain

少し順番が前後してしまって見づらいかもしれませんが、番号の順に説明していきます。

  1. ロールアウト初期化
    主にロールアウト変数の初期化を行っています。
    self: このロールアウト自身のインスタンスを保持する為の変数です。理由は前回の記事を参照してください。
    subRollouts: サブロールアウトのインスタンスを保持する配列です。こちらも理由は前回の記事で説明した通りです。
    btmControls: サブロールアウトより下にあるコントロールの配列を格納しています。リサイズ時に、サブロールアウトの高さに合わせて位置を移動させる必要が有るため、予めリスト化しておきます。
    最後にresize()関数を呼び出してウィンドウサイズを初期化します。
  2. サブロールアウト追加・削除関数
    単純にサブロールアウトの追加と削除を行う関数ですが、追加後にサブロールアウトのresizeCallbackに親のresize関数オブジェクトを代入しています。
    こちらも前回の記事で説明していますが、ロールアウトは実体化のタイミングで変数が全てクリアされてしまうので、このタイミングで代入する必要があります。
  3. サイズ変更関数
    全ての子ロールアウトの高さを計算し、SubRolloutと親ロールアウトの高さ、それからbtmControlsに格納したコントロールのY位置を調整しています。
    各ロールアウトの高さを1px単位で微調整していますが、Maxのバージョンによってはこの辺りの細かい数値は変わってきてしまうかもしれません。この値は2014に最適化しています。
    maxHeightの値を変えることによってサブロールアウトの最大高さを変える事ができます。
  4. サブロールアウトON/OFFボタン変更イベント
    ボタンのON/OFF状態を見て、サブロールアウトを追加したり削除したりしています。
    最後にresize()を呼び出して、ロールアウトサイズを調整しています。
  5. パラメータ取得ボタンクリックイベント
    現在のサブロールアウトのパラメータを取得してメッセージボックスに出力します。
self変数とsubRollouts変数は一見意味が無いように見えますが、前回の記事のとおり、ロールアウトを多重起動した時に、確実に自分自身のインスタンスを参照する為に必要な変数です。
これをやっておかないと、ロールアウト多重起動時に正しく処理できなくなってしまいます。

ちょっと長くなってしまいましたが、こんな感じです。

最後に

どちらかというと、MaxよりはMayaっぽい見た目のウィンドウになりますが、パラメータをロールアウト毎にグループ化しておいて、最下部の"実行"ボタンで処理を実行したりと、何かと便利に使える形式だと思います。
使う側から見ても、視線を上から下に移動しながら最後に実行ボタンを押せるので、UIの基本として理にかなっているかと思います。

今回作成したコードはこちらからダウンロードする事ができます。
コードに特に使用制限などは設定しませんので、自由に改変等して使って頂ければ幸いです。

MaxScriptTip: ロールアウト挙動メモ

2月 21, 2016 0
ロールアウトの挙動がわりと意味不明だったので、勉強も兼ねて簡単に調べてみました。
  1. ロールアウトは定義されたタイミングで単一のオブジェクトとなる。
    クラス・インスタンスという概念はなく、プロセス内で1つの実体しか持てない。
    また、1つのオブジェクトにつき1つのウィンドウしか作れない。
    試しにcreateDialogを2度実行しても、作成されるダイアログは1つのみ。
    複数のSubRolloutやFloaterで実体を作成しようとしても、既に別の場所で実体化されているロールアウトは作成されない。
  2. 作成されたロールアウトは、同名の変数に格納される。
    例えばTestRolloutならTestRollout変数に格納される。
    普通の変数なので代入などで上書きする事ができる。
    また、作成したロールアウトオブジェクトを別の変数に入れ替える事もできる。
  3. 複数の実体を作りたい時には、再度定義コードを実行しなければならない。
    定義を実行すると、新規にロールアウトオブジェクトが作成され、同名の変数に上書きされる。
    前に作成したオブジェクトへの参照が失われる為、後から参照したいときは、予め変数からオブジェクトを取り出しておく必要がある。
    例えばSubRollout内の子要素を参照したいとき等、新しく作成した方のロールアウトを参照してしまい、意図しない挙動をする事がある。
  4. copy関数等でオブジェクトを複製する事は出来るが、それらは同一のロールアウトを参照しており、やはり多重起動できない。
  5. ロールアウトはcreateDialog等で実体化する時に、再度初期化される。
    実体化前に変数や関数にアクセスする事は出来るが、実体化時に再度初期化される。
全体的にかなりよく分からない仕様になっていますが、重要なのはオブジェクトと変数を切り離して考える事、クラス・インスタンスの概念が存在しない事を把握する事かと思います。
Max自体が20年以上も前に設計されたソフトだという事もあり、いわゆるオブジェクト指向的な考え方には則っていないのだと思います。

MaxScriptTip: Rolloutでのドラッグ&ドロップの受け取りについて

2月 17, 2016 0
今回はRollout内での外部からのドラッグ&ドロップ受け取りについて。

MaxScriptのRolloutは、残念ながらファイルのドラッグ&ドロップがサポートされていません。
仕方がないので、代わりに.NETコントロールを作成し、.NET側で発生したイベントを受け取ってこの機能を実装します。

.NETでのドラッグ&ドロップ

.NETは本来Windowsアプリを開発する為に設計されたフレームワークなので、全ての種類のコントロールでドロップ受け取りが可能です。
例えばラベル、ボタン、パネル系コントロールやリストボックス等でも受け取る事ができます。

今回はラベルを使用します。

rollout rltDropTest "DropDialog" width:160 height:72
(
    -- 1. コントロール作成
    dotNetControl dotNetLabel "label" pos:[0,0] width:160 height:72 
 
    -- 2. ロールアウト初期化イベント
    on rltDropTest open do
    (
        local Color = dotnetclass "System.Drawing.Color"
        local Alignment = dotNetClass "System.Drawing.ContentAlignment"
        dotNetLabel.allowdrop = true
        dotNetLabel.text = "Drop files here."
        dotNetLabel.backcolor = Color.FromArgb 255 64 64 64
        dotNetLabel.forecolor = Color.white
        dotNetLabel.textAlign = Alignment.MiddleCenter
    )
    
    -- 3. ドラッグカーソル受け入れイベント
    on dotNetLabel DragEnter sender args do
    (
        local Formats = dotnetclass "DataFormats"
        local Effects = dotnetclass "DragDropEffects"
        if args.Data.GetDataPresent Formats.FileDrop then
            args.Effect = Effects.Copy
        else
            args.Effect = Effects.None
    )
    
    -- 4. ドロップイベント
    on dotNetLabel DragDrop sender args do
    (
        local Formats = dotnetclass "DataFormats"
        local fileNames = args.Data.GetData Formats.FileDrop false
        print fileNames
    )
)

createDialog rltDropTest

実行すると以下の様なウィンドウが表示されます。
このウィンドウにファイルをドロップすると、そのファイルパスがリスナーに出力されます。

スクリプト説明

  1. コントロール作成
    .NETラベルコントロールを作成しています。
    この時、ラベルがRollout全体を覆うようにサイズ調整しています。
  2. ロールアウト初期化イベント
    ロールアウトopenイベント内でラベルコントロールの設定をしています。
    特にallowdropはtrueにしておかないとドロップイベントが発生しないので要注意です。
    その他プロパティは特に必要では無いのですが、コントロールの見栄えを良くするために設定しています。
    FromArgbの引数は、字のごとくAlpha, Red, Green, Blueの順です。
  3. ドラッグカーソル受け入れイベント
    このハンドラは.NETコントロールのDragEnterイベントを受け取っています。
    引数のsenderはイベントを発生させたコントロール(この場合はdotNetLabelのインスタンス)、argsはDragEventArgsクラスのインスタンスを格納しています。
    DragEnterイベントは、ドラッグ中のカーソルがコントロール上に侵入した時に発生し、そのドラッグを受け入れ可能かをユーザーに示す為に使用します。
    ここではドラッグのデータタイプをチェックし、ファイルであれば受け入れるように処理しています。
    その他に、ビットマップやテキストデータ等の受け取りも可能です。
  4. ドロップイベント
    ユーザーが最終的にドロップした時に発生します。
    イベント引数はDragEnterと同じです。
    ここではDragEventArgsからドロップされたファイルリストを取得し、fileNamesに格納しています。
    fileNamesは受け取った全てのファイルのフルパスを含む配列です。

情報

その他、各クラス詳細は.NETのリファレンスを参照してください。

Label クラス
Color 構造体
ContentAlignment 列挙体
DataFormats クラス
DragDropEffects 列挙体
DragEventArgs クラス

おまけ

.msファイル暗号化ツール作ってみました。

TR_EncryptScript.ms ver1.00

MaxScriptTip: MaxScriptEditorで等幅フォントを使う

2月 07, 2016 0
MaxScriptエディターでスクリプトを書いていて最も気になるのは、やはりフォントが等幅になっていない事でしょう。
メニューには一応「等幅フォントを使用」がありますが、実際には日本語環境では正しく表示してくれません。

というわけで、今回は等幅フォントの導入法について。

基本的に、MaxScriptエディタで等幅フォントを利用するには、グローバル設定ファイルでフォントを等幅フォントに変更するだけですが、問題はどのフォントを利用するのかという事です。


オススメフォント

個人的にオススメなのは、メイリオを等幅化したMeiryoKeフォントです。

日本語環境での等幅フォントで人気があるのはコーディングに特化したRictySource Han Code JP等ですが、どちらも標準のWindowsレンダリングエンジンでは美しくフォントをレンダリング出来ず、所々欠けた汚いフォントが出力されてしまいます。

その点、MeiryoKeはメイリオをベースにしているので、Windows環境でも完璧なフォントをレンダリングしてくれるのが嬉しい所です。

MeiryoKeはライセンス諸問題でフォントファイルを自力で出力せねばならず、少々面倒な部分もありますが、マニュアルにしたがって作業すれば簡単に出力できるので、とくに困ることもないでしょう。

というわけで、実際にMSゴシックとMeiryoKeを比較してみた画像がこちらです。



好みの問題もあるかもしれませんが、少なくとも日本語含めて等幅になっているのが分かります。
MeiryoKeの方はフォントの特性上、9ポイント以下に設定すると少々見づらいので10ポイントに設定していますが、好みでサイズを調整してもいいと思います。


フォントの導入

導入手順は以下のようになります。
  1. MeiryoKeの出力スクリプトを入手し、自力でフォントを出力する。
    ダウンロード先はこちら
    Windows10の場合は試して無いので分かりませんが、恐らく8.1用で大丈夫なんじゃないかなぁと思います。もしくは過去のバージョンのメイリオをダウンロードしてきてもいいかと。
  2. 3dsMaxのインストールフォルダの使用者権限を「フルコントロール」に設定する。
    もしくはMaxを管理者権限で起動してもいいのですが、いちいち面倒なので私はフォルダの権限の方を変更してしまいます。
    1. Maxのインストールフォルダのプロパティを開きセキュリティタブに移動。
    2. 「編集」ウィンドウを開きユーザーを選択。
    3. 「フルコントロール」にチェックを入れ「OK」
  3. Maxを起動し、MSエディタのグローバル設定ファイルを変更する。
    1. MaxScriptエディタのメニューから「ツール」→「グローバル オプション ファイルを開く」。
    2. font.base=font:Verdana,size:10~といった部分を探し、「Verdana」部分をMeiryoKe_Gothicに変更する。
    3. そこから数行にわたってのフォント設定も全てMeiryoKe_Gothicに変更する。
    4. フォントサイズも全て10に変更する。
    5. 上書き保存。
      (ファイル変更権限を取得してないとここで失敗する。)
これでフォントが変更されます。

グローバル設定ファイルは細かく弄っていくともっといろいろなカスタマイズが出来るので、気が向いたら色々触ってみてもいいかもしれません。



MaxScriptTip: skinOpsをUndoに記録する

2月 07, 2016 0
MaxScriptを使っていると、たまにUndoに記録されないコマンドがある事に気づくと思います。
例えばタイトルにあるようにskinOps。(3dsMax2014及び2015で確認)

某所ではskinOpsはAutodeskがインターンに作らせた欠陥コマンドなんて言われてたりしますが(実際問題多い)、今回はそのコマンドをUndoに記録する方法について紹介します。

結論から言うとUndoに記録されるコマンドと一緒に使って、undo on ()で括ってしまえば記録されるようになります。

undo "SetWeight" on
(
    theSkin = $.modifiers[#skin]
    skinOps.ReplaceVertexWeights theSkin 1 #(1) #(1.0)
)
記録されない。

undo "SetWeight" on 
(
    theSkin = $.modifiers[#skin]
    skinOps.bakeSelectedVerts theSkin  -- Undo記録用
    skinOps.ReplaceVertexWeights theSkin 1 #(1) #(1.0)
)
記録される。

う~ん、この欠陥っぷり・・・。

ちなみに今回はUndo記録用にbakeSelectedVertsを使っていますが、これはUndoに記録されるskinOps内のコマンドで最も高速に動作するからだそうです。

今回はbakeSelectedVertsを使いましたが、他にUndoに記録されるコマンドを既に使っているのであれば、わざわざ使う必要はありません。

自分もこの問題でかなり悩まされたので、誰かの参考になればいいなと。


MaxScriptTip: デバッグ情報を文字列化する

1月 26, 2016 0
MaxScriptでスタックトレースを出力するにはstack()を使用します。
fn testFunc =
(
    try
    (
        local var1 = "Text Message"
        local var2 = 1234.567
        local var3 = var1 + var2
    )
    catch
    (
        local ss = StringStream ""
  
        format "Error Message:\n%\n\n" (getCurrentException()) to:ss
        append ss "StackTrace:\n"
        stack to:ss
  
        format "%\n" (ss as string)
    )
)
testFunc()

出力
Error Message:
-- 変換できません : 1234.57 入力: String

StackTrace:
** スレッド データ : スレッドID : 14032
** ------------------------------------------------------
** [スタック レベル : 0]
** イン testFunc(); ファイル名 : C:\Scripts\StackTrace.ms; 位置 : 327; 行番号 : 15
--  ローカル :
--   var3: undefined
--   ss: StringStream:""
--   var1: "Text Message"
--   var2: 1234.57
--  外部 :
--   owner: undefined
** ------------------------------------------------------
** [スタック レベル : 1]
** 呼び出されました トップレベル

スクリプトファイル内に仕込んで、エラー発生時にユーザーに例外情報を通知して貰ったりっていう機能に利用できますね。

ただ、その場合はスタックトレースにファイルパスが含まれますので、ユーザー名が含まれないように置き換えたり、暗号化したりと工夫が必要かもしれません。

なんにせよ、getCurrentExceptionだけでは全く情報が足りないので、例外通知するならかなり有効ですね。


ブログ移転しました

1月 26, 2016 0
もう多分しません。

というわけで旧ブログリンク
http://mayatech.blog.jp/


今後のブログ方針について

今後は3dsMaxの技術関係の記事を書いていけたらと思います。
主にMaxScript。

残念ですが前ブログで扱っていたMaya関係は多分殆ど扱いません。
Mayaスクリプトの保守も多分しません。

だって仕事が(r