How To > Develop a Bitmap Painting Tool- Unwrap UV Coordinates |
In this step of the Bitmap Painting tool development, we will add the option to generate a graphical representation of the UV coordinates of an object inside the painting tool and assign automatically as diffuse map to the scene object.
--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
To calculate the position of the UV coordinates inside the bitmap, we have to count the pixels as the size of the bitmap minus one (for example, 0 to 511 and not 1 to 512). To make out life easier, we define two variables to store these corrected values.
local temp_bitmap_filename = (getDir #preview +"/microPaint_temp.tga") local theCanvasBitmap = bitmap bitmapX bitmapY color:white filename:temp_bitmap_filename
To be able to save the current painting to a temporary file on disk, we have to define a file name and assign it to the bitmap. The file name is stored in a local variable that will be used in other parts of the script to access it. TGA appears to be faster to save than BMP, but you can save to any format you want.
local theBackgroundBitmap = bitmap bitmapX bitmapY color:white local currentPos = lastPos = [0,0] localtheChannel = 1
This is the map channel to read from. You can add a User Interface spinner to control its value. For now, we will work with channel 1 only.
This variable will store the mesh object to access the UV coordinates from.
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..." )
We add a new menu item that will be used to acquire texture coordinates from a scene object.
on commit_menu picked do copy theCanvasBitmap theBackgroundBitmap on uv_menu picked do MicroPaint_CanvasRollout.unwrapTexture()
If the user picked the menu item, we call a new function that will be defined inside the rollout (see below).
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
After saving to a new file name, set back the filename to the temporary location, otherwise autosaving starts writing into the new file and we do not want this to happen.
) ) 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
This function allows us to pick only geometry objects except target objects. It will be used as a filter function by a new pickbutton User Interface control.
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
To accommodate the new autoSave option, we increase the across parameter to 6.
This new checkbutton will control whether the painting is constantly saved to disk after each stroke.
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
The default brush size is now 1. This is to avoid drawing a texture unwrapping with a thickness of 10 that can be very slow.
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
This pickbutton will be used to select the scene object to unwrap the texture coordinates. It uses the new optional keyword autoDisplay added to 3ds Max 7. When set to true, it will display the name of the picked object on the button automatically. In 3ds Max 6 and earlier, the option will be ignored, but the script will still function correctly.
This is the event handler of the pickbutton. If the user picked an object, it will be passed to the handler in the obj variable.
If the obj variable contains a valid object (the user did not cancel the picking) then,
we first assign the picked object to our new variable that will store the pick during the painting session.
try ( copy theObj.material.diffusemap.bitmap theCanvasBitmap copy theObj.material.diffusemap.bitmap theBackgroundBitmap theCanvas.bitmap = theCanvasBitmap
Then, we try to read the diffusemap bitmap of the object and copy it into the canvas and background bitmaps of the painting tool, and assign it to the UI.
If there is no bitmap there, the error will be trapped and nothing will happen.
)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: = (
We add a new optional keyword to the drawStroke function. If it is set to false, the stroke will be drawn, but the bitmap will not be updated in the rollout. If it is true or not supplied, the bitmap will be updated after the stroke. This is to allow for faster drawing of the unwrapped texture without updating the view after each texture face.
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
If the new switch is set to true or not set at all, we update the display as before, otherwise the update is skipped.
This is the new function that performs the selection of an object, the unwrapping of the texture coordinates, and the material/texture assignment.
If the user clicked a valid object,
we extract the mesh from the top of its stack.
If the selected object supports the specified channel,
we read the number of texture faces in that channel,
and repeat the following code for every face found in the mesh:
This is the map face definition. It returns a Point3 value where .x, .y, and .z are indices pointing at the texture vertices the face is using.
vert1= meshop.getMapVert theMesh theChannel theFace.x vert2= meshop.getMapVert theMesh theChannel theFace.y vert3= meshop.getMapVert theMesh theChannel theFace.z
Using this data, we read the three vertices of the face. Each one is a Point3 value containing the actual U, V, and W values. We will draw only the UV and discard the W.
drawStroke [vert1.x * bitmapx_1, bitmapy_1- vert1.y * bitmapy_1] [vert2.x * bitmapx_1, bitmapy_1- vert2.y * bitmapy_1] drawIt:false
We call the drawStroke function using the first and second texture vertex coordinates multiplied by the size of the bitmap. The flag drawIt is set to false to speed up the generation of the image. If you change it to true, you can watch the function drawing interactively in the image, but this can take a very long time.
The calculation of the coordinates is based on the fact that the lower left corner of the UV space has coordinates 0,0 and the upper right has coordinates 1,1.
The bitmap on the other hand has its upper left corner as 0,0 and its lower right corner as bitmapx-1, bitmapy-1 (511,511 in our example).
So, to convert the UV coordinates in pixel coordinates, we multiply the width resp. height of the bitmap by the U resp. V value, and turn the vertical result upside-down by subtracting from the height.
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
We call the drawStroke function for the other two pairs of vertices, thus drawing all edges of the face.
Now, we can update the rollout bitmap to show the result.
We can also save the bitmap to its temporary location.
If the selected object has no material, we assign a new material.
try (if theObj.material.diffusemap == undefined do theObj.material.diffusemap = bitmapTexture filename:temp_bitmap_filename
If the selected object's diffusemap is not defined yet, we try to assign a new texture map pointing at the temporary copy of our painting. If the object does not have a standard material assigned, trying to access the diffusemap channel will cause an error, which will be prevented by the try()catch() context.
We press the Show Map In Viewport of the material at object level. If the texture assignment is successful, the bitmap must be shown in the viewport.
If everything went ok, we can also turn on the new autosave option to force the bitmap to update the selected object as you paint.
If the user checked the new checkbutton, the bitmap will be saved to its temporary location immediately.
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
If the new checkbutton is pressed, the bitmap will be saved to its temporary location after each stroke.
) on MicroPaint_CanvasRollout rbuttonup pos do ( isErasing = isDrawing = false if autoSave.checked do save theCanvasBitmap
If the new checkbutton is pressed, the bitmap will be saved to its temporary location after each erase stroke.
) 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 )
In this example, Unwrap UVW modifier is applied to a teapot and the Flatten function is used to create the new texture coordinates.
Then, the object is selected in MicroPaint and the texture coordinates are drawn using the Circular Smooth brush with size 5, Airbrush turned on, and Airbrush Speed set to 1.0.
This is the teapot as seen in the viewport:
Now, you can start painting on top of the UV coordinates using all available brushes - as long as the AutoSave button is checked, all your strokes must update in the viewports: