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

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

ビットマップ ペイント ツール開発の手順では、3ds Max ビューポート内のシーン オブジェクトに直接ペイントするオプションを追加します。

全体の流れ:

ユーザ インタフェースに、3D ペイントの有効と無効を切り替えるチェックボタンを追加します。

PainterInterface を使用して、ビューポート内でブラシをコントロールする単純な関数をいくつか定義し、ペイントする面の 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.tga")
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
 
local bary = [0,0,0]
local faceIndex = 1
 
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..."
    menuItem paint3d_menu "Toggle 3D Painting..."
  )
  on commit_menu picked do copy theCanvasBitmap theBackgroundBitmap
 
  on uv_menu picked do MicroPaint_CanvasRollout.unwrapTexture()
  on paint3d_menu picked do MicroPaint_CanvasRollout.startPainting3D()

  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_too lpicked 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 offset:[0,-3] highlightcolor:(color 255 200 200)
  checkbutton airBrush "AirBrush" width:70 offset:[0,-3] highlightcolor:(color 200 255 200)
  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
 
  pickbutton pickMesh "Pick Mesh" width:90 height:30 highlightcolor:(color 200 200 255) 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() 
    ) 
  ) 
 
  checkbutton paint3D "3D PAINT" width:90 height:50 highlightcolor:(color 200 200 255) pos:[bitmapX+5,180]
 
  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
  )
 
  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_1, 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
    )
  )
 
 
  fn StartStroke = ( thePainterInterface.undoStart() )
  fn PaintStroke =
  (
    theMesh = theObj.mesh
    thePainterInterface.getHitFaceData &bary &faceIndex theObj 0
    theFace = meshop.getMapFace theMesh theChannel faceIndex
    vert1= meshop.getMapVert theMesh theChannel theFace.x
    vert2= meshop.getMapVert theMesh theChannel theFace.y
    vert3= meshop.getMapVert theMesh theChannel theFace.z
    thePoint = bary.x*vert1 + bary.y*vert2 + bary.z*vert3
    drawStroke [thePoint.x * bitmapx_1, bitmapy_1 - thePoint.y * bitmapy_1] [thePoint.x * bitmapx_1, bitmapy_1 - thePoint.y * bitmapy_1]
    thePainterInterface.clearStroke()
  )
  fn EndStroke =
  (
    thePainterInterface.undoAccept()
    if autoSave.checked do
    (
      save theCanvasBitmap
      try(theObj.material.diffsemap.bitmap = theCanvasBitmap)catch()
    ) 
  )
 
  fn CancelStroke = (thePainterInterface.undoCancel())
 
  fn SystemEndPaintSession = ( paint3D.checked = false )
 
  fn startPainting3D =
  (
    if thePainterInterface.InPaintMode() or theObj == undefined then
    (
      thePainterInterface.EndPaintSession()
      paint3D.checked = false
    )
    else
    (
      paint3D.checked = true
      thePainterInterface.pointGatherEnable = TRUE
      thePainterInterface.initializeNodes 0 #(theObj)
      thePainterInterface.offMeshHitType = 2
      thePainterInterface.ScriptFunctions startStroke paintStroke endStroke cancelStroke SystemEndPaintSession
      thePainterInterface.startPaintSession()
    )
  )
 
  on paint3D changed state do startPainting3D()
 
  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 autoSave changed state do if state 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

local temp_bitmap_filename = (getDir #preview +"/microPaint_temp.tga")
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 local bary = [0,0,0]

この変数は、PainterInterface が面内のヒット ポイントの重心座標を返すために使用します。

local faceIndex = 1

この変数は、PainterInterface がヒットした面のインデックスを返すときに使用します。

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..." menuItem paint3d_menu "Toggle 3D Painting..."

新しい[Toggle 3D Painting]のメニュー項目です。メインのユーザ インタフェースにも、よく目立つボタンを追加します。

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

新しいメニュー項目[Toggle 3D Painting]のハンドラです。PainterInterface のオンとオフを切り替える関数を呼び出します。

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 offset:[0,-3] highlightcolor:(color 255 200 200)
checkbutton airBrush "AirBrush" width:70 offset:[0,-3] highlightcolor:(color 200 255 200)
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
 
pickbutton pickMesh "Pick Mesh" width:90 height:30 highlightcolor:(color 200 200 255) 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()
)
) checkbutton paint3D "3D PAINT" width:90 height:50 highlightcolor:(color 200 200 255) pos:[bitmapX+5,180]

3D 空間でのペイントをコントロールする新しいチェックボタンです。有効にすると、青色に変わります。

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
)

on pickMesh picked obj do if obj != undefined do theObj = Obj

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_1, 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
)
)
 
fn StartStroke = ( thePainterInterface.undoStart() )

この関数はストロークの開始時点で実行されます。詳細は、「PainterInterface の説明」を参照してください。

fn PaintStroke =
(

この関数は、3D でストロークのペイントを行います。

theMesh = theObj.mesh

ペイントするオブジェクトの TriMesh のコピーを取得します。

thePainterInterface.getHitFaceData &bary &faceIndex theObj 0

次にこの関数を呼び出し、ブラシが最後にヒットした面を取得します (最後の引数は 0 という面インデックスで、「最後のヒットを取得する」ことを意味します)。スクリプトの初めの方で定義した変数 bary および faceIndex は、「参照によって」渡されます。 つまり、関数の結果はこれらに書き出されます。呼び出し後、bary には面内のヒット ポイントの重心座標が入り、faceIndex には最後にヒットした面のインデックスが入ります。 theObj は、ペイント対象としてシーン内で選択したオブジェクトを入れる変数です。

theFace = meshop.getMapFace theMesh theChannel faceIndex

このデータを使用し、ヒットしたメッシュの面と同じインデックスを持つチャネル 1 のマップ面を取得します。

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

次に、マップ面が参照するテクスチャ頂点を読み取ります。

thePoint= bary.x*vert1 + bary.y*vert2 + bary.z*vert3
 drawStroke [thePoint.x * bitmapx_1, bitmapy_1 -thePoint.y * bitmapy_1] [thePoint.x * bitmapx_1, bitmapy_1 -thePoint.y * bitmapy_1]

面内のヒット ポイントの重心座標と、その面に対応する テクスチャ頂点の座標を使用して、ヒット ポイントの UV 座標を計算します。

thePainterInterface.clearStroke()

最後に、ストロークをクリアします。

) fn EndStroke = (

この関数はストロークの終了時点で呼び出されます。

thePainterInterface.undoAccept()

アンドゥ システムの変更を受け入れます。

if autoSave.checked do (

ただし自動保存が有効になっている場合です。

save theCanvasBitmap

ペイントをディスクに保存します。

try(theObj.material.diffsemap.bitmap = theCanvasBitmap)catch()

オブジェクトのマテリアルの拡散反射光マップ チャネルのビットマップに、ペイントのビットマップ値を強制設定します。

) 
)

fn CancelStroke = (thePainterInterface.undoCancel())

この関数は、ストロークをキャンセルした場合に呼び出されます。

fn SystemEndPaintSession = ( paint3D.checked = false )

この関数は、ペイントのセッションが終了したときに呼び出されます。ペイントが別の理由で終了する場合に、UI ボタンをオフにします。

fn startPainting3D =
(

この関数は、3D ペイントのオンとオフを切り替えます。

if thePainterInterface.InPaintMode() or theObj == undefined then
(

すでにペイント中である場合、またはオブジェクトがまだ選択されていない場合は、

  thePainterInterface.EndPaintSession()
  paint3D.checked = false

3D ペイントのセッションを終了し、UI のボタンのチェックマークを外します。

) 
else
(

3D ペイントが有効になっていないときにメッシュ シーン オブジェクトが選択されている場合は、

paint3D.checked = true

チェックボタンのチェックマークが必ず付いた状態になっているようにします (メイン メニューからチェックマークを付けるのと同じです)。

thePainterInterface.pointGatherEnable = TRUE

ポイント収集モードを有効にします。

thePainterInterface.initializeNodes 0 #(theObj)

選択されている 1 つのオブジェクトだけで PainterInterface を初期化します。

thePainterInterface.offMeshHitType = 2

メッシュがヒットされない場合の動作を 2 に設定します。この動作ではメッシュは必要ではありません。この場合にはヒットされる面はなく、ペイントが行われません。

thePainterInterface.ScriptFunctions startStroke paintStroke endStroke cancelStroke SystemEndPaintSession

事前に定義済みのスクリプト関数をすべて PainterInterface に登録し、

thePainterInterface.startPaintSession()

ペイントを開始します。

) 
) 

on paint3D changed state do startPainting3D()

ユーザがチェックボタンを押した場合に上の関数を呼び出します。

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 autoSave changed state do if state 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
)

結果:

3D のオブジェクトと 2D のイメージは同時にペイントできます。

前のチュートリアル:

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

関連事項