チュートリアル - ビットマップ ペイント ツールの開発 - 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 座標のアンラップ