チュートリアル - ビットマップ ペイント ツールの開発 - UV 座標のアンラップ

このビットマップ ペイント ツール開発の手順では、ペイント ツール内にあるオブジェクトの UV 座標のグラフィック表示を生成するオプションを追加し、それを拡散反射光マップとしてシーン オブジェクトに自動的に割り当てます。

全体の流れ:

作業対象のメッシュ オブジェクトを格納するためのローカル変数を追加します。

メッシュ オブジェクトだけを選択するための新しいフィルタ関数を追加します。

作業対象オブジェクトを選択するための新しい pickbutton を UI に追加します。 この pickbutton でフィルタ関数を使用します。

テクスチャ座標を取得するためのオプションを[編集] (Edit)メニューに追加します。

選択したメッシュから UV 座標を読み込み、テクスチャ空間の頂点位置に基づいてストロークを描画する、新しい関数を追加します。

ペイントの既定の一時ファイル名を定義し、オプションでそれを各ストロークの後に保存します。

ビットマップの一時ディスク コピーは、選択したオブジェクトの拡散反射光チャネルに割り当てられます。

ユーザ インタフェースに、自動保存の有効と無効を切り替えるための新しいチェックボックスを追加します。有効にすると、ビットマップはツールでペイントが行われるのに並行してリアルタイムで更新されます。

スクリプト:

    macroScript MicroPaint category: "HowTo"
    (
    global MicroPaint_CanvasRollout
    try (destroyDialog MicroPaint_CanvasRollout) catch()
    local isErasing = isDrawing = false
    local bitmapX = bitmapY = 512
    local bitmapx_1 = bitmapx-1
    local bitmapy_1 = bitmapy-1
    local temp_bitmap_filename = (getDir #preview +"/microPaint_temp.bmp")
    local theCanvasBitmap = bitmap bitmapX bitmapY color:white filename:temp_bitmap_filename
    local theBackgroundBitmap = bitmap bitmapX bitmapY color:white
    local currentPos = lastPos = [0,0]

    local theChannel = 1
    local theObj = undefined

    rcMenu CanvasMenu
    (
      subMenu "File"
      (
        menuItem new_menu "New"
        menuItem open_menu "Open..."
        menuItem save_as "Save As..."
        separator file_menu_1
        menuItem quit_tool "Quit"  
      )
      subMenu "Edit"
      (
        menuItem commit_menu "Commit Changes"
        separator edit_menu_1
        menuItem uv_menu "Get UV Coordinates..."
      )
      on commit_menu picked do copy theCanvasBitmap theBackgroundBitmap
      on uv_menu picked do MicroPaint_CanvasRollout.unwrapTexture()

      subMenu "Help"
      (
        menuItem about_tool "About MicroPaint..."
      )

      on new_menu picked do
      (
        theBackgroundBitmap = theCanvasBitmap = bitmap bitmapX bitmapY color:MicroPaint_CanvasRollout.paperColor.color filename:temp_bitmap_filename
        MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
      )

      on open_menu picked do
      (
        theOpenBitmap= selectBitmap()
        if theOpenBitmap != undefined do
        (
          copy theOpenBitmap theCanvasBitmap
          copy theOpenBitmap theBackgroundBitmap
          close theOpenBitmap
          MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
        )
      )

      on save_as picked do
      (
        theSaveName = getSaveFileName types:"BMP (*.bmp)|*.bmp|Targa (*.tga)|*.tga|JPEG (*.jpg)|*.jpg"
        if theSaveName != undefined do
        (
          theCanvasBitmap.filename = theSaveName
          save theCanvasBitmap
          theCanvasBitmap.filename = temp_bitmap_filename
        )
      )
      on about_tool picked do messagebox "MicroPaint\nMAXScript Tutorial" title:"About..."
      on quit_tool picked do destroyDialog MicroPaint_CanvasRollout
    )

    fn mesh_filter obj = superclassof obj == GeometryClass and classof obj != TargetObject

    rollout MicroPaint_CanvasRollout "MicroPaint"
    (
      bitmap theCanvas pos:[0,0] width:bitmapX height:bitmapY bitmap:theCanvasBitmap
      colorpicker inkColor height:16 modal:false color:black across:6
      colorpicker paperColor height:16 modal:false color:white
      checkbutton autoSave "AutoSave" width:70
      checkbutton airBrush "AirBrush" width:70
      spinner AirBrushSpeed "Speed" range:[0.1,50,10] fieldwidth:30
      spinner BrushSize "Size" range:[1,50,1] type:#integer fieldwidth:40
      listbox BrushShape items:#("Circle","Box","Circle Smooth") pos:[bitmapX+5,0] width:90


      --NEW PICK BUTTON AND HANDLER:
      pickbutton pickMesh "Pick Mesh" width:90 height:30 pos:[bitmapX+5,140] filter:mesh_filter autodisplay:true

      on pickMesh picked obj do
      (
        if obj != undefined do
        (
          theObj = Obj
          try
          (
            copy theObj.material.diffusemap.bitmap theCanvasBitmap
            copy theObj.material.diffusemap.bitmap theBackgroundBitmap
            theCanvas.bitmap = theCanvasBitmap
          )catch()
        )
      )
      --END NEW PICK BUTTON AND HANDLER

      fn paintBrush pos =
      (
        if isErasing then
          thePaintColor = (getPixels theBackgroundBitmap pos 1)[1]
        else
          thePaintColor = inkColor.color
        if thePaintColor == undefined then thePaintColor = white

        case BrushShape.selection of
        (
          1: (
            if distance pos currentPos <= BrushSize.value/2 do
              setPixels theCanvasBitmap pos #(thePaintColor)
            )
          2: setPixels theCanvasBitmap pos #(thePaintColor)
          3: (
            theFactor = (distance pos currentPos) / (BrushSize.value/2.0)
            if theFactor <= 1.0 do
            (
              theFactor = sin ( 90.0 * theFactor)
              thePixels = getPixels theCanvasBitmap pos 1
              if thePixels[1] != undefined do
              (
                thePixels[1] = (thePixels[1] * theFactor) + (thePaintColor * (1.0 - theFactor))
                setPixels theCanvasBitmap pos thePixels
              )
            )
          )--end case 3
        )--end case
      )--end fn

      fn drawStroke lastPos pos drawIt: =
      (
        currentPos = lastPos
        deltaX = pos.x - lastPos.x
        deltaY = pos.y - lastPos.y
        maxSteps = amax #(abs(deltaX),abs(deltaY))
        deltaStepX = deltaX / maxSteps
        deltaStepY = deltaY / maxSteps
        for i = 0 to maxSteps do
        (
          if airBrush.checked then
            for b = 1 to (BrushSize.value / AirBrushSpeed.value) do
              paintBrush (currentPos + (random [-BrushSize.value/2,-BrushSize.value/2] [BrushSize.value/2,BrushSize.value/2] ))
          else
            for b = -BrushSize.value/2 to BrushSize.value/2 do
              for c = -BrushSize.value/2 to BrushSize.value/2 do
                paintBrush (currentPos + [c,b])
          currentPos += [deltaStepX, deltaStepY]
        )
        if drawIt== true or drawIt == unsupplied do theCanvas.bitmap = theCanvasBitmap
      )


      --NEW UNWRAP TEXTURE FUNCTION STARTS HERE
      fn unwrapTexture =
      (
        if theObj != undefined then
        (
          theMesh = snapshotAsMesh theObj
          if meshop.getMapSupport theMesh theChannel do
          (
            faceCount = meshop.getNumMapFaces theMesh theChannel
            for f = 1 to faceCount do
            (
              theFace = meshop.getMapFace theMesh theChannel f
              vert1= meshop.getMapVert theMesh theChannel theFace.x
              vert2= meshop.getMapVert theMesh theChannel theFace.y
              vert3= meshop.getMapVert theMesh theChannel theFace.z
              drawStroke [vert1.x * bitmapx_1, bitmapy_1 - vert1.y * bitmapy_1] [vert2.x * bitmapx_1, bitmapy_1 - vert2.y * bitmapy_1] drawIt:false
              drawStroke [vert1.x * bitmapx_1, bitmapy_1 - vert1.y * bitmapy_1] [vert3.x * bitmapx_1, bitmapy_1 - vert3.y * bitmapy_1] drawIt:false
              drawStroke [vert3.x * bitmapx_1, bitmapy_1 - vert3.y * bitmapy_1] [vert2.x * bitmapx, bitmapy_1 - vert2.y * bitmapy_1] drawIt:false
            )
          )
          theCanvas.bitmap = theCanvasBitmap
          save theCanvasBitmap
          if theObj.material == undefined do theObj.material = Standard()
          if theObj.material.diffusemap == undefined do
            theObj.material.diffusemap = bitmapTexture filename:temp_bitmap_filename
          showTextureMap theObj.material true
          autoSave.checked = true
        )
      )
      --NEW UNWRAP TEXTURE FUNCTION ENDS HERE

      on autoSave changed state do if state do save theCanvasBitmap

      on MicroPaint_CanvasRollout lbuttondown pos do
      (
        lastPos = pos
        isDrawing = true
        isErasing = false
        drawStroke lastPos pos
      )
      on MicroPaint_CanvasRollout rbuttondown pos do
      (
        lastPos = pos
        isErasing = isDrawing = true
        drawStroke lastPos pos
      )
      on MicroPaint_CanvasRollout lbuttonup pos do
      (
        isErasing = isDrawing = false
        if autoSave.checked do save theCanvasBitmap
      )
      on MicroPaint_CanvasRollout rbuttonup pos do
      (
        isErasing = isDrawing = false
        if autoSave.checked do save theCanvasBitmap
      )

      on MicroPaint_CanvasRollout mousemove pos do
      (
        if isDrawing do drawStroke lastPos pos
        lastPos = pos
      )
    )
    createDialog MicroPaint_CanvasRollout (bitmapx+100) (bitmapy+30) menu:CanvasMenu
    MicroPaint_CanvasRollout.theCanvas.bitmap = theBackgroundBitmap
    )

ステップごとの解説

--Code in italic has no changes since the previous version. macroScript MicroPaint category:"HowTo"
(
global MicroPaint_CanvasRollout
try(destroyDialog MicroPaint_CanvasRollout)catch()
local isErasing = isDrawing = false
local bitmapX = bitmapY = 512

local bitmapx_1 = bitmapx-1
local bitmapy_1 = bitmapy-1

ビットマップ内の UV 座標の位置を計算するには、ピクセルをビットマップのサイズ - 1 としてカウントする必要があります (たとえば 1 ~ 512 ではなく、0 ~ 511)。簡単にするために、これらの正しい値を格納する変数を 2 つ定義します。

local temp_bitmap_filename = (getDir #preview +"/microPaint_temp.tga")
local theCanvasBitmap = bitmap bitmapX bitmapY color:white filename:temp_bitmap_filename

現在のペイントをディスク上の一時ファイルに保存できるようにするには、ファイル名を定義してビットマップに割り当てる必要があります。ファイル名はローカル変数に格納され、スクリプトの他の箇所でそのファイルにアクセスする場合にはそれが使用されます。BMP よりは TGA で保存した方が速くできるようですが、保存はどの形式で行ってもかまいません。

local theBackgroundBitmap = bitmap bitmapX bitmapY color:white
local currentPos = lastPos = [0,0] localtheChannel = 1

読み込み対象のマップ チャネルです。この値をコントロールするための、ユーザ インタフェース スピナーを追加することができます。この時点ではチャンネル 1 のみを扱います。

local theObj = undefined

この変数は、UV 座標にアクセスするメッシュ オブジェクトを格納します。

rcMenu CanvasMenu
(
subMenu "File"
(
menuItem new_menu "New"
menuItem open_menu "Open..."
menuItem save_as "Save As..."
separator file_menu_1
menuItem quit_tool "Quit"  
)
subMenu "Edit"
(
menuItem commit_menu "Commit Changes"

separator edit_menu_1
menuItem uv_menu "Get UV Coordinates..."
)

シーン オブジェクトからテクスチャ座標を取得するために使用する、新しいメニュー項目を追加します。

on commit_menu picked do copy theCanvasBitmap theBackgroundBitmap on uv_menu picked do MicroPaint_CanvasRollout.unwrapTexture()

ユーザがこのメニュー項目を選択した場合、新しい関数を呼び出します。新しい関数はロールアウト内に定義します(下記参照)。

subMenu "Help"
(
menuItem about_tool "About MicroPaint..."
)

on new_menu picked do
(
theBackgroundBitmap = theCanvasBitmap = bitmap bitmapX bitmapY color:MicroPaint_CanvasRollout.paperColor.color filename:temp_bitmap_filename
MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
)

on open_menu picked do
(
theOpenBitmap= selectBitmap()
if theOpenBitmap != undefined do
(
copy theOpenBitmap theCanvasBitmap
copy theOpenBitmap theBackgroundBitmap
close theOpenBitmap
MicroPaint_CanvasRollout.theCanvas.bitmap = theCanvasBitmap
)
)

on save_as picked do
(
theSaveName = getSaveFileName types:"BMP (*.bmp)|*.bmp|Targa (*.tga)|*.tga|JPEG (*.jpg)|*.jpg"
if theSaveName != undefined do
(
theCanvasBitmap.filename = theSaveName
save theCanvasBitmap theCanvasBitmap.filename = temp_bitmap_filename

新しいファイル名で保存した後、ファイル名を元の一時的な場所に戻します。こうしないと、自動保存により新しいファイルに書き出しが行われてしまいます。

)
)
on about_tool picked do messagebox "MicroPaint\nMAXScript Tutorial" title:"About..."
on quit_tool picked do destroyDialog MicroPaint_CanvasRollout
)

fn mesh_filter obj = superclassof obj == GeometryClass and classof obj != TargetObject

この関数により、ジオメトリ オブジェクトだけが選択できるようになります。 ただし、ターゲット オブジェクトは選択できません。この関数は新しいユーザ インタフェース コントロールである pickbutton でフィルタ関数として使用されます。

rollout MicroPaint_CanvasRollout "MicroPaint"
(
bitmap theCanvas pos:[0,0] width:bitmapX height:bitmapY bitmap:theCanvasBitmap

colorpicker inkColor height:16 modal:false color:black across:6

新しい自動保存オプションが収まるように、across パラメータを 6 に増やします。

colorpicker paperColor height:16 modal:false color:white

checkbutton autoSave "AutoSave" width:70

この新しいチェックボタンでは、ペイントが各ストロークの後で常にディスクに保存されるかどうかをコントロールします。

checkbutton airBrush "AirBrush" width:70
spinner AirBrushSpeed "Speed" range:[0.1,50,10] fieldwidth:30 spinner BrushSize "Size" range:[1,50,1] type:#integer fieldwidth:40

既定のブラシ サイズは 1 になりました。10 の厚さでアンラップしたテクスチャの描画を避けるためです。10 の厚さではテクスチャ アンラップの描画にとても時間がかかる可能性があります。

listbox BrushShape items:#("Circle", "Box", "Circle Smooth") pos:[bitmapX+5,0] width:90

pickbutton pickMesh "Pick Mesh" width:90 height:30 pos:[bitmapX+5,140] filter:mesh_filter autodisplay:true

この pickbutton は、テクスチャ座標をアンラップするシーン オブジェクトを選択するときに使用するものです。3ds Max 7 で追加された新しいオプションのキーワード、autoDisplay を使用します。true に設定されると、選択されたオブジェクトの名前が自動的にボタンに表示されます。3ds Max 6 以前のリリースでは、このオプションは無視されますが、それでもスクリプトは正しく機能します。

Pickbutton

on pickMesh picked obj do
(

これは、pickbutton のイベント ハンドラです。ユーザがオブジェクトを選択した場合、obj 変数のハンドラに渡されます。

if obj != undefined do (

obj 変数に有効な値が入っている場合(ユーザが選択をキャンセルしなかった場合)、

theObj = Obj

まず選択オブジェクトを新しい変数に割り当てます。この変数はペイント セッションの間、選択を保存します。

try
(
copy theObj.material.diffusemap.bitmap theCanvasBitmap
copy theObj.material.diffusemap.bitmap theBackgroundBitmap
theCanvas.bitmap = theCanvasBitmap

次に、オブジェクトの diffusemap ビットマップを読み込み、それをペイント ツールのキャンバスおよびバックグラウンドのビットマップにコピーして UI に割り当てます。

ビットマップがない場合、エラーはトラップされ何も起こりません。

)catch()
)
)

fn paintBrush pos =
(
if isErasing then
thePaintColor = (getPixels theBackgroundBitmap pos 1)[1]
else
thePaintColor = inkColor.color
if thePaintColor == undefined then thePaintColor = white

case BrushShape.selection of
(
1: (
if distance pos currentPos <= BrushSize.value/2 do
setPixels theCanvasBitmap pos #(thePaintColor)
)
2: setPixels theCanvasBitmap pos #(thePaintColor)
3: (
theFactor = (distance pos currentPos) / (BrushSize.value/2.0)
if theFactor <= 1.0 do
(
theFactor = sin ( 90.0 * theFactor)
thePixels = getPixels theCanvasBitmap pos 1
if thePixels[1] != undefined do
(
thePixels[1] = (thePixels[1] * theFactor) + (thePaintColor * (1.0 - theFactor))
setPixels theCanvasBitmap pos thePixels
)
)
)--end case 3
)--end case
)--end fn
  fn drawStroke lastPos pos drawIt: =
(

drawStroke 関数に新しいオプションのキーワードを追加します。このキーワードが false に設定された場合、ストロークは描画されますが、ロールアウト内のビットマップは更新されません。このキーワードが true の場合、またはこのキーワードを省略した場合は、ビットマップはストロークの後に更新されます。これは、テクスチャ面ごとにビューを更新せずに、アンラップされたテクスチャをより速く描画できるようにするためです。

currentPos = lastPos
deltaX = pos.x - lastPos.x
deltaY = pos.y - lastPos.y
maxSteps = amax #(abs(deltaX),abs(deltaY))
deltaStepX = deltaX / maxSteps
deltaStepY = deltaY / maxSteps
for i = 0 to maxSteps do
(
if airBrush.checked then
for b = 1 to (BrushSize.value / AirBrushSpeed.value) do
paintBrush (currentPos + (random [-BrushSize.value/2,-BrushSize.value/2] [BrushSize.value/2,BrushSize.value/2] ))
else
for b = -BrushSize.value/2 to BrushSize.value/2 do
for c = -BrushSize.value/2 to BrushSize.value/2 do
paintBrush (currentPos + [c,b])
currentPos += [deltaStepX, deltaStepY]
) if drawIt== true or drawIt == unsupplied do theCanvas.bitmap = theCanvasBitmap

新しいスイッチが true に設定された場合、または何も設定されない場合、前と同じように表示を更新します。それ以外の場合、更新はスキップされます。

)

fn unwrapTexture =
(

オブジェクトの選択、テクスチャ座標のアンラップ、およびマテリアル/テクスチャ割り当てを実行する新しい関数です。

if theObj != undefined then
(

ユーザが有効なオブジェクトをクリックした場合、

theMesh =snapshotAsMeshtheObj

スタックの最上位からメッシュを抽出します。

if meshop.getMapSupport theMesh theChannel do
(

選択オブジェクトが指定されたチャネルをサポートする場合、

faceCount = meshop.getNumMapFaces theMesh theChannel

そのチャネル内のテクスチャ面の数を読み込み、

for f = 1 to faceCount do
(

メッシュ内の面のそれぞれについて、次のコードを繰り返します。

theFace = meshop.getMapFace theMesh theChannel f

これは、マップ面の定義です。Point3 値を返します。x、y、z はその面が使用するテクスチャ頂点を指すインデックスです。

vert1= meshop.getMapVert theMesh theChannel theFace.x
vert2= meshop.getMapVert theMesh theChannel theFace.y
vert3= meshop.getMapVert theMesh theChannel theFace.z

このデータを使用して、面の 3 つの頂点を読み込みます。それぞれが、実際の U、V、W の値を含む Point3 値です。ただし、描画するのは UV だけで、W は破棄されます。

drawStroke [vert1.x * bitmapx_1, bitmapy_1- vert1.y * bitmapy_1] [vert2.x * bitmapx_1, bitmapy_1- vert2.y * bitmapy_1] drawIt:false

最初と 2 つ目のテクスチャ頂点の座標をビットマップのサイズで乗じたものを使用して drawStroke 関数を呼び出します。drawIt フラグは、イメージ生成を加速するために false に設定されます。フラグを true に設定すると、関数がイメージに対してインタラクティブに描画されますが、かなり時間がかかる場合があります。

座標の計算は、UV 空間の左下隅が 0,0 で、右上隅が 1,1 という座標であるという事実に基づいて行われます。

もう一方の手のビットマップの左上隅は 0,0 で、右下隅は bitmapx-1,bitmapy-1 です (この例では 511,511)。

UV 座標をピクセル座標に変換するには、ビットマップの幅および高さを U および V の値でそれぞれ乗算し、高さから引くことで垂直方向の高さを逆にします。

drawStroke [vert1.x * bitmapx_1, bitmapy_1- vert1.y * bitmapy_1] [vert3.x * bitmapx_1, bitmapy_1- vert3.y * bitmapy_1] drawIt:false
drawStroke [vert3.x * bitmapx_1, bitmapy_1- vert3.y * bitmapy_1] [vert2.x * bitmapx_1, bitmapy_1- vert2.y * bitmapy_1] drawIt:false

残りの 2 つの頂点についても drawStroke 関数を呼び出します。こうして面のすべてのエッジを描画します。

)
)
theCanvas.bitmap = theCanvasBitmap

ここでロールアウトのビットマップを更新し、結果を表示します。

save theCanvasBitmap

また、ビットマップを一時的な場所に保存します。

if theObj.material == undefined do theObj.material = Standard()

選択したオブジェクトにマテリアルがない場合、新しいマテリアルを割り当てます。

try
(if theObj.material.diffusemap == undefined do
theObj.material.diffusemap = bitmapTexture filename:temp_bitmap_filename

選択したオブジェクトの diffusemap がまだ定義されていない場合は、ペイントの一時コピーを指す新しいテクスチャ マップを割り当てます。オブジェクトに標準的なマテリアルが割り当てられていない場合、diffusemap チャネルにアクセスを試みるとエラーが発生します。これは try()catch() コンテキストで防止します。

showTextureMap theObj.material true

マテリアルのオブジェクト レベルで[ビューポート内でマップを表示](Show Map In Viewport)を押します。テクスチャの割り当てに成功した場合、ビットマップがビューポートに表示されます。

autoSave.checked = true

すべて問題なく進んだ場合、新しい自動保存オプションをオンにして、選択したオブジェクトが描画に並行してビットマップで強制的に更新されるようにします。

)catch()
)
)

on autoSave changed state do if state do save theCanvasBitmap

ユーザが新しいチェックボタンを選択した場合、ビットマップはただちに一時的な保存場所に保存されます。

on MicroPaint_CanvasRollout lbuttondown pos do
(
lastPos = pos
isDrawing = true
isErasing = false
drawStroke lastPos pos
)
on MicroPaint_CanvasRollout rbuttondown pos do
(
lastPos = pos
isErasing = isDrawing = true
drawStroke lastPos pos
)
on MicroPaint_CanvasRollout lbuttonup pos do
(
isErasing = isDrawing = false if autoSave.checked do save theCanvasBitmap

新しいチェックボタンが押されている場合、ビットマップは各ストロークの後に一時的な保存場所に保存されます。

)
on MicroPaint_CanvasRollout rbuttonup pos do
(
isErasing = isDrawing = false

if autoSave.checked do save theCanvasBitmap

新しいチェックボタンが押されている場合、ビットマップは各ストロークの消去後に一時的な保存場所に保存されます。

 )

on MicroPaint_CanvasRollout mousemove pos do
(
if isDrawing do drawStroke lastPos pos
lastPos = pos
)
)
createDialog MicroPaint_CanvasRollout (bitmapx+100) (bitmapy+30) menu:CanvasMenu
MicroPaint_CanvasRollout.theCanvas.bitmap = theBackgroundBitmap
)

結果:

この例では、ティーポットに[UVW アンラップ](Unwrap UVW)モディファイヤが適用され、新しいテクスチャ座標を作成するためにフラッテン関数が使用されています。

その後、MicroPaint でオブジェクトが選択され、サイズ 5 の円形のスムーズ ブラシと、オンになって[Airbrush Speed]が 1.0 に設定されたエアブラシを使用してテクスチャ座標が描画されています。

ティーポットは、ビューポート内に次のように表示されます。

ここで、利用可能なブラシをどれでも使用して UV 座標の上にペイントすることができます。[AutoSave]ボタンが選択されていれば、ストロークはすべてビューポート内で更新されます。

前のチュートリアル:

チュートリアル - ビットマップ ペイント ツールの開発 - 変更の消去

次のチュートリアル:

チュートリアル - ビットマップ ペイント ツールの開発 - 3D ペイント