ごちゃペディア

はなまるデジタル創作紀行(DTM、TAS、いろいろな技術)

ピコカキコ - WAV13によるPCM再生の考え方

WAV13によるPCM再生とは何か、例えばこのピコカキコを聴いてみてください。

両サイドで鳴っているパッドの音、それにドラムの音、とても綺麗です。どちらもトリッキーな鳴らし方をしているようですが、この記事ではドラムで使われている技法を主に追求します。

ピコカキコ WAV13 PCM利用補助ツールを作成しました。後述の面倒な計算が一瞬で行えます。実用的な面だけ知りたい人は、まずツールのページを読んで、それから情報を本記事で補うと良いかもです。

続きを読む

MIDIトラック分割ツール「MidiSplit」を作りました

MidiSplit project page (Download) - Divide MIDI track by channel number & program number

MIDIファイルを読み込んで、楽器ごとにトラックを分割するツールを作りました。ツールの概要ですが、下記の画像をご覧ください。

MidiSplit は主にチャンネル数を制限したシーケンスの再配置に有効です。チャンネル数を節約しているシーケンスでは曲中でプログラムチェンジを送ることで、1チャンネル(1トラック)で複数のパートを演奏していることがあります。そのようなシーケンスをアレンジしようとする場合、パート(楽器)ごとに閲覧や調整ができたほうが便利ですが、プログラムチェンジが頻繁に配置されている場合は分割するのに手間がかかります。

MidiSplit はトラックに混在する各パートの演奏を手早く楽器ごとに分割します。

続きを読む

DeSmuMEに3D表示抑制機能を足してみた

Download Windows binary & source code (desmume 0.9.8-gfx3dHack)

ゲーム中のキャラクタの画像だけとか、地形の画像だけキャプチャしたい時ってありますよね。他のエミュレータと同じくDeSmuMEにもレイヤー表示・非表示を切り替える機能はあるのですが、ゲームによっては前景から背景に至るまで同一レイヤーで3D描画していることがあり、この場合には前述の機能は役に立ちません。

じゃあ何かしら役に立つ機能をつけてみようというのが本日の趣旨です。

改造の概要

「3D描画を部分的に抑制したい」というのが今回求める機能です。

内部的には3Dデータは多数のポリゴンで構成されています。これを位置情報に基づいて、3Dエンジンが背面〜前面にかけて描画することで処理されます。表示を抑制したければ、一部のポリゴンの描画処理をあえて行わなければ良いのです。簡単ですね。

レイヤーと同様にポリゴンの表示・非表示機能をつければ良さそうですが、困ったことにポリゴンの数は軽く100を超えるくらいには多いです。個別切り替えでは使い勝手もよくなさそうなので、「前の方のポリゴンを非表示」「後ろの方のポリゴンを非表示」という形で対応することにしました。「前の方」「後ろの方」の度合いを切り替えできるようにすることで、抑制度合に柔軟性をもたせます。

コードと使い方

0.9.8 のソースコードとの差分を作成して、改造版を作成しました。ダウンロードリンクは記事の冒頭にあります。

新しくダイアログを設けるのは実装の手間がかかるので、表示の抑制はLuaスクリプトを通じて行う形にしました。

-- desmume 0.9.8-gfx3dHack sample script

local gfx3dMin, gfx3dMax = 0.000000, 1.000000
function modifyVisibility()
	gui.text(0, 0, string.format("%f %f", gfx3dMin, gfx3dMax))
	gui.setgfx3dvisibility(gfx3dMin, gfx3dMax)
end

local keys = { {}, {} }
gui.register(function()
	keys[1] = input.get()

	if keys[1]["6"] then
		gfx3dMin = math.min(math.max(gfx3dMin - 0.001, 0.0), 1.0)
	end
	if keys[1]["7"] then
		gfx3dMin = math.min(math.max(gfx3dMin + 0.001, 0.0), 1.0)
	end
	if keys[1]["8"] then
		gfx3dMax = math.min(math.max(gfx3dMax - 0.001, 0.0), 1.0)
	end
	if keys[1]["9"] then
		gfx3dMax = math.min(math.max(gfx3dMax + 0.001, 0.0), 1.0)
	end

	modifyVisibility()

	keys[2] = keys[1]
end)

emu.registerexit(function()
	gui.setgfx3dvisibility(0.0, 1.0)
end)

追加した関数は1つだけです。

function gui.setgfx3dvisibility(gfx3dStart, gfx3dEnd)
-- gfx3dStart: 0.0 = no effect, 1.0 = hide all polygons
-- gfx3dEnd: 0.0 = hide all polygons, 1.0 = no effect

3Dポリゴンの表示割合を設定します。最前面・最背面にあるポリゴンの一部を隠すことができるようになっていて、例えば 0.2, 0.9 と指定すると、全ポリゴンのうち、最背面にある 20% のポリゴンと、最前面にある 10% (100%-90%) のポリゴンは表示されなくなります。サンプルスクリプトでは、数字の 6, 7, 8, 9 キーを使ってこの表示割合を変更できるようにしてあります(スクリプト自体の利便性は高くないので、目的に応じて改良することができると思います)。

感想

万能ではないですが、良い感じに欲しい画像が得られるようになりました。

総ポリゴン数は細かい状況の違いによって簡単に変動するので、少しゲームを操作すると表示がちらついて、思うような表示結果が継続されないという利便性の問題があります。不便さはありますが、数値を細かく調整する、欲しいフレームを狙ってキャプチャするなど、工夫次第で目的は果たせるかなあと思います。

あとは背景色が自前で指定できるとか、描画のない部分はアルファチャンネル付きの透明ピクセルとして保存できるようにするとか、そんな対応があると背景の除去や合成がぐっとしやすくなると思うのですが、今日のところは放っておくことにします。誰か興味があれば改良してくれると喜びます。

それでは、良いリッピングライフをお楽しみください☆

追伸

背景色の操作ですが、Memory Viewer で Palette 0 を操作したら変更できました。15 bit color かな?

  5000000h Engine A Standard BG Palette (512 bytes)
  5000200h Engine A Standard OBJ Palette (512 bytes)
  5000400h Engine B Standard BG Palette (512 bytes)
  5000600h Engine B Standard OBJ Palette (512 bytes)
  7000000h Engine A OAM (1024 bytes)
  7000400h Engine B OAM (1024 bytes)
  http://nocash.emubase.de/gbatek.htm#dsmemorycontrolvram

Soft Rasterizer はピクセルずれを起こすのでオススメ出来ません。OpenGLレンダリングしましょう。条件によっては選びたいエンジンが変わるかもしれませんが。

Lua-GD でのアルファチャンネルの扱い

Lua-GD のアルファチャンネルの扱いでよく失敗するのでまとめました。勘違いしていると、じつは画像が透明だったり、不透明だったり、半透明があるのに思い通りにブレンドされなかったりします。

内容は Lua-GD 2.0.33r2 に準拠しています。他の環境下での挙動は保証しません。

画像の新規作成 (フルカラー)

  • フルカラー画像の作成には、gd.createTrueColor(x, y) を利用。
  • 画像作成後は、全面不透明黒で塗られた状態(透明ではない)
  • 画像作成後は、gdImage:alphaBlending(true) の状態。
  • 画像作成後は、gdImage:saveAlpha(false) の状態。
  • アルファチャンネルの範囲は 0〜127 であり、0〜255 ではないので注意。
  • 透明なキャンバスを作成するには透明ピクセルで塗りつぶす必要がある。下記にフルカラー透過画像を作成するコードを示す。
-- create a blank truecolor image
gd.createTrueColorBlank = function(x, y)
	local gdImage = gd.createTrueColor(x, y)
	if gdImage == nil then return nil end

	local colorTrans = gdImage:colorAllocateAlpha(255, 255, 255, 127)
	gdImage:alphaBlending(false)
	gdImage:filledRectangle(0, 0, gdImage:sizeX() - 1, gdImage:sizeY() - 1, colorTrans)
	gdImage:alphaBlending(true)
	gdImage:colorDeallocate(colorTrans)
	return gdImage
end

余談ですが、gd.create(gdImage) 的な関数はありそうでないです。

画像の読込 (PNG)

  • PNG画像の読込には、gd.createFromPng(filename) を利用。
    • ファイル内容を文字列をとして読み込んである場合は gd.createFromPngStr(string) を利用。
  • パレットかフルカラーかは入力ファイルに基づく(要注意!

画像の保存 (PNG)

  • PNG画像の保存には、gdImage:png(filename), gdImage:pngEx(filename, compression_level) を利用。
  • 出力ファイルがパレットかフルカラーかは入力画像に基づく。
  • アルファチャンネルの保存は gdImage:saveAlpha(true) を実行していないと行われない。

画像のコピー・貼り付け

  • 画像の一部を転写するには gd.copy(dstImage, srcImage, dstX, dstY, srcX, srcY, w, h) を利用。
  • フルカラー画像は dstImage:alphaBlending(boolean) の設定により挙動が異なる。true であればブレンディングを行うが、false であればアルファ値を含めてピクセルをそのままコピーする*1
  • フルカラーとパレットの混在時には注意を要する(後述)

パレットとフルカラー

フルカラーとパレットの混在時には注意を要する場合がある。

  • 画像がフルカラーかどうか調べるには、下記のようなメソッドを要する。
gd.isTrueColor = function(gdImage)
	if gdImage == nil then return nil end
	local gdStr = gdImage:gdStr()
	if gdStr == nil then return nil end
	return (gdStr:byte(2) == 254)
end
  • パレットの「透過色」とフルカラーの「アルファ」は異なる概念である。それ故か下記の問題がある。
    • gd.copy() で パレット→フルカラー のコピーを行うとき、gdImage:alphaBlending(boolean) に関わらず透過ピクセルが透過コピーされてしまう。ただし、gd.copyResampled() であれば gdImage:alphaBlending(boolean) を尊重したコピーが行われる。
  • 上記を考慮すると、画像をフルカラーに変換する下記のコードが書ける。
-- return a converted image
gd.convertToTrueColor = function(srcImage)
	if srcImage == nil then return nil end
	if gd.isTrueColor(srcImage) then return srcImage end

	local gdImage = gd.createTrueColor(srcImage:sizeX(), srcImage:sizeY())
	if gdImage == nil then return nil end

	gdImage:alphaBlending(false)
	local colorTrans = gdImage:colorAllocateAlpha(255, 255, 255, 127)
	gdImage:filledRectangle(0, 0, gdImage:sizeX() - 1, gdImage:sizeY() - 1, colorTrans)
	gdImage:copyResampled(srcImage, 0, 0, 0, 0, gdImage:sizeX(), gdImage:sizeY(), gdImage:sizeX(), gdImage:sizeY())
	gdImage:alphaBlending(true)

	return gdImage
end

おまけ:画像の左右反転

  • 拡大コピーで負数を指定
  • 反転画像の作成は、新しいGDライブラリにはあるが、上記 Lua-GD にはない。ただし下記で代用できる(巨大画像では低速)
-- flip an image about the vertical axis
gd.flipVertical = function(gdImage)
	if gdImage == nil then return nil end
	gdImage:alphaBlending(false)
	for x = 0, gdImage:sizeX() do
		for y = 0, math.floor(gdImage:sizeY()/2) - 1 do
			local c1, c2 = gdImage:getPixel(x, y), gdImage:getPixel(x, gdImage:sizeY()-1-y)
			gdImage:setPixel(x, y, c2)
			gdImage:setPixel(gdImage:sizeX()-1-x, y, c1)
		end
	end
	gdImage:alphaBlending(true) -- TODO: restore the previous value
	return gdImage
end
-- flip an image about the horizontal axis
gd.flipHorizontal = function(gdImage)
	if gdImage == nil then return nil end
	gdImage:alphaBlending(false)
	for y = 0, gdImage:sizeY() do
		for x = 0, math.floor(gdImage:sizeX()/2) - 1 do
			local c1, c2 = gdImage:getPixel(x, y), gdImage:getPixel(gdImage:sizeX()-1-x, y)
			gdImage:setPixel(x, y, c2)
			gdImage:setPixel(gdImage:sizeX()-1-x, y, c1)
		end
	end
	gdImage:alphaBlending(true) -- TODO: restore the previous value
	return gdImage
end
-- applies vertical and horizontal flip
gd.flipBoth = function(gdImage)
	-- use of gd.copyRotated() can provide the same result?
	gd.flipVertical(gdImage)
	gd.flipHorizontal(gdImage)
	return gdImage
end

おまけ:EmuLua

  • 今日以前の desmume は gui.gdscreenshot() で透明な画像を返す
    • gd.createFromPngStr(gd.createFromGdStr(gui.gdscreenshot()):pngStr()) と書けば1行でアルファを切り捨て可能

*1:srcImage が完全に透明な画像を考えるとわかりやすい。true の際は dstImage に変化はないが、false の際は dstImage のコピー部分が透明ピクセルに変化する。