SculptGenMax
Overview
Here's a 3ds Max 9 script to create sculptie textures.
The forum thread for this is here.
NOTE: I'm not saying that this is a better way to do it. If you are using a method that bakes textures and it's working for you, there is no reason to try this.
This algorithm generates the texture from the vertices. In contrast to most baking methods which generate the texture from the faces. It's not clear to me yet if one method is better than the other.
Anyway, instructions are in the script. It will *not* work on an arbitrary mesh. To simplify things, I used a particular cylinder or sphere mesh as my starting point. Instructions for creating the mesh are in the script.
Feel free to modify it for your own personal use and post bugs and fixes in the forum thread or the comments section below.
Features
- Vertex-based
- Cylinder and Sphere
- Editable Poly, Editable Mesh, and NURBS
- Preserves UV Mapping
- Preserves orientation and proportion in X, Y, Z
Revision History
Latest Release 0.4.1
Links to Related Threads
SculptGenMax for 3dsMax9 [1]
3ds Max - Sculpted Prims exporter Petition / pledges [2]
sculpts + quad/vertex duality question w/ blender [3]
3DsMax Tutorial for sculpt prim [4]
3DS Max Tutorial: Sculptie Egg [5]
Comments
Put comments, questions, feature requests, and bug reports here. --Shack Dougall 14:03, 30 May 2007 (PDT)
The Script
-- *************************************************************************************** -- SculptGenMax -- Copyright 2007 Shack Dougall -- -- This is a MAXScript written for 3ds Max 9. It might work for earlier versions of 3ds Max, -- but this has not been tested. It does not work for Max5. -- -- The script generates a Second Life sculptie texture from the selected object. -- -- Revision History -- ==================== -- 0.2 First released version -- 0.2.1 Added proportional -- 0.2.2 Fixed rotation issues -- -- 0.3 Shifted bitmap by 1 pixel in X and Y. changed defaults "NO SMOOTH" 128x128 -- no longer have to move the top and bottom vertices when you create the object. -- -- 0.3.1 added x:, y:, and z: display boxes, so that you can know what dimensions to use -- on your prim if you have the proportional feature turned off -- -- 0.4 a) sculptmap now matches the 3dsMax UV map. -- You should now be able to bake textures using the UV map and they -- should line up with the sculptie in SL. NOTE: because I now use the UV map, if -- your sculptie looks inside out, then your UV map is probably wrong. -- b) only one vertex is used in the top and bottom row of the cylinder. The vertices -- used are the top and bottom vertices on the seam of the UV map, -- i.e., the vertices at the top left and bottom left of the UV map. -- c) sculptmap now has one more row of data. The cylinder should be 32x32. -- sculptmap conforms to http://forums.secondlife.com/showpost.php?p=1522861 -- d) smooth feature removed. wasn't helpful. -- e) 32x32 option removed. 64x64 is the lowest resolution sculptmap supported. -- I might change this later, but it needs work to support 32x32 properly. -- f) now supports a NURBS cylinder. See description below. -- 0.4.1 a) fixed bug with UV map. Now handles UV maps correctly that have more vertices than are -- in the mesh. -- b) Poly sphere 32x32 supported. -- c) NURBS Sphere 32x32 supported. -- d) added drop down to allow you to change the uv map channel that is used. -- *** CREATING THE POLY/MESH OBJECT *** -- The script assumes a specific topology for the object. You **CANNOT** use the script on -- an arbitrary mesh. -- -- Create the following object as a starting point: -- 1) create a cylinder with 32 height segments and 32 side segments -- 2) Apply an Edit Mesh modifier -- 3) delete the vertex in the top and the vertex in the bottom -- -- Now, you can move the vertices any way that you like. -- -- When you're done, make sure that the object is selected and generate the texture. -- -- NOTE: as of Version 0.4.1, you can also use a sphere. You need a 32x32 poly or mesh sphere. -- The easiest way to create this is to follow the directions for the NURBS sphere -- and then convert to mesh or poly. -- *** CREATING THE NURBS OBJECT *** -- As of Version 0.4, you can also use a NURBS cylinder as your starting object. -- As of Version 0.4.1, you can use a NURBS Sphere. -- -- 1) Create a cylinder or sphere -- 2) Convert to NURBS -- 3) If cylinder (skip for sphere), expand the "NURBS Surface" modifier and select "Surface" under it. -- Select and delete the top and bottom of the cylinder. -- 3) Go back to "NURBS Surface". -- In the NURBS "Surface Approximation" Rollout, select Tessellation Method "Regular" -- and set U Steps and V Steps to 32. -- Now, you can NURB away. Hint: Select NURBS Surface>Surface CV to see control vertices -- When you're done, make sure that the object is selected and generate the texture. -- *** SCRIPT INSTALLATION *** -- MAXScript>Run Script... -- in the dialog, select the script and press Open. -- Customize>Customize User Interface... -- click on the Toolbars tab. Select Category "SecondLife". Drag SculptGenMax to one of the -- toolbars. -- -- *** RUNNING THE SCRIPT *** -- Install script -- Create object, modify it as desired, and select it. -- Press SculptGenMax in the toolbar where you installed it. -- A dialog will pop up. Select desired bitmap resolution. Then on the dialog's menu bar, -- File>Generate Sculpt Texture -- this will generate the texture in the dialog. -- File>Save as.. -- to save the texture to a file. -- -- *** UNINSTALLING THE SCRIPT *** -- Right click on the SculptGenMax button in the toolbar and select "Delete Button". -- Find the file "SecondLife-SculptGenMax.mcr" and delete it. -- Restart 3ds Max -- -- *************************************************************************************** macroScript SculptGenMax category:"SecondLife" ( local PROG_NAME = "SculptGenMax" local PROG_VERSION = "0.4.1" global SculptGenMax_CanvasRollout try(destroyDialog SculptieGenMax_CanvasRollout)catch() local default_keep_proportions = true -- determines the default value of the "proportional" checkbox local default_uv_map_channel = 1 -- the default map channel from which we get the uv coordinates. local default_resolution = 2 --determines the default selection of the resolution listbox local defaultBitmapSize = 128 --determines the default resolution. -- IMPORTANT: if you change default_resolution, then you must also change defaultBitmapSize local keep_proportions = default_keep_proportions -- if true, we preserve the proportions of the model -- otherwise, each dimension scales independently local MAX_BITMAP = 256 local BITMAP_X = BITMAP_Y = defaultBitmapSize -- dimensions of the bitmap to be generated local MESH_ROWS = 31 -- number of rows in mesh, excluding poles local MESH_COLS = 32 -- number of columns in mesh local UV_MAP_CHANNEL = default_uv_map_channel -- The map channel from which we get the UV coordinates 1 by default. local POLE_V_LIMIT = #( 0.02, 0.98 ) -- A vertex must have a V value less than 0.02 or greater than -- 0.98 in order to be considered a pole vertex. -- A 64x64 bitmap is the smallest fully-specified sculptmap. So, we need to know how much -- bigger our current bitmap is than a 64x64 map local bitMapTo64RatioX = BITMAP_X / 64; local bitMapTo64RatioY = BITMAP_Y / 64; local HORIZ_FLIP = false; -- set this to true to flip the texture horizontally local sculptBitmap = bitmap BITMAP_X BITMAP_Y color:white -- generate the bitmap here local blankBitmap = bitmap BITMAP_X BITMAP_Y color:white -- used to reset the dialog local minDimensions = [0,0,0] --minimum X, Y, and Z values in the mesh local maxDimensions = [0,0,0] --maximum X, Y, and Z values in the mesh local maxScale -- the maximum difference between the min and max in any single dimension local propPrimSize = [0,0,0] --if proportional is off, this is the size we should make the prim in SL -- so that it is proportional local selectedPoly -- contains a copy of the user's selection. we read the mesh data from here -- it must be deleted after we are done or it will show up as a new object -- in the scene local firstRow = #() -- the indexes of the vertices in the top row of the bitmap local lastRow = #() -- the indexes of the vertices in the bottom row of the bitmap -- prevRow and currRow are used while iterating over the mesh local prevRow = #() -- contains the indexes of the vertices in the previous full row of the bitmap local currRow = #() -- contains the indexes of the vertices in the row of the bitmap that we are -- drawing right now. local edgeCount = #( 0, 0, 0, 0, 0, 0, 0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ) --local debug = newScript() -- uncomment previous line if you want to use print statements for debugging -- examples of debug statements. These will show up in the debug window if the debug line is uncommented -- print "foo" to:debug -- format "x = %\n" vertex.x to:debug rcMenu CanvasMenu ( subMenu "File" ( menuItem generate "Generate Sculpt Texture" menuItem save_as "Save As..." separator file_menu_1 menuItem quit_tool "Quit Dialog" ) subMenu "Edit" ( ) subMenu "Help" ( menuItem about_tool "About..." ) fn calcPrimSizeToKeepProportions = ( propPrimSize = [0, 0, 0] propPrimSize.x = (maxDimensions.x - minDimensions.x) / maxScale propPrimSize.y = (maxDimensions.y - minDimensions.y) / maxScale propPrimSize.z = (maxDimensions.z - minDimensions.z) / maxScale propPrimSize ) fn scaleX xValue xMin xMax = ( local xScale = (xValue - xMin) / (xMax - xMin) if keep_proportions then ( xScale = (xScale - 0.5) * ((xMax - Xmin) / maxScale) + 0.5 ) local result = floor (xScale * 255 + 0.5) result ) --scaleX fn vertexToColor vertPos = ( local vertColor = [127,127,127] vertColor.x = scaleX vertPos.x minDimensions.x maxDimensions.x vertColor.y = scaleX vertPos.y minDimensions.y maxDimensions.y vertColor.z = scaleX vertPos.z minDimensions.z maxDimensions.z vertColor ) --vertexToColor fn maxOf value1 value2 = ( local maxValue if value1 >= value2 then maxValue = value1 else maxValue = value2 maxValue ) -- maxOf fn minOf value1 value2 = ( local minValue if value1 <= value2 then minValue = value1 else minValue = value2 minValue ) -- minOf fn getVertexPos vertId = ( local aPos = selectedPoly.GetVertex vertId (in coordsys world aPos) * selectedPoly.objecttransform ) fn getUVMapVerticesFor vertId = ( -- we have to do this because the vertIds in the poly do not correspond directly to the -- vertIds in the UV map. In fact, the relationship between poly verts and uv map verts is -- one to many. i.e., each poly vert can be represented by 1 or more uv map verts. -- -- to find the map verts, we get the poly faces for the poly vert. Then, we get the -- corresponding uv map faces. And this gives us the map verts. -- -- this works because the faceIds in the poly have a one-to-one correspondence with -- the faceIds in the UV map. -- -- get the poly vert's faces. local faceIds = (polyOp.getFacesUsingVert selectedPoly vertId) as array local mapVerts = #() for faceId in faceIds do ( -- get the vertIds for this poly face local polyFaceVerts = polyOp.getFaceVerts selectedPoly faceId -- get the vertIds for this map face local mapFaceVerts = polyOp.getMapFace selectedPoly UV_MAP_CHANNEL faceId -- find the index of the poly vert in the poly face. polyVertIndex = findItem polyFaceVerts vertId -- get the cooresponding mapVertId by using the same index on the mapFace local mapVertId = mapFaceVerts[polyVertIndex] -- put the mapVertId in mapVerts, if we haven't already if findItem mapVerts mapVertId == 0 do append mapVerts mapVertId ) -- at the end of this we have an array of all the map vertices that correspond to the poly vert mapVerts ) fn getUVpos vertId = ( -- returns the uv coords for the vertex. If the vertex has more than one UV coordinate, -- then we return the one with the lowest U value. local mapVerts = getUVMapVerticesFor vertId local uvPos = [2,0] for mapVert in mapVerts do ( local tempPos = polyOp.getMapVert selectedPoly UV_MAP_CHANNEL mapVert if tempPos.x < uvPos.x then uvPos = tempPos ) uvPos ) fn pass1 = ( -- in Pass 1, we iterate over all vertices, collecting max/min x,y, and z -- and we find the two end points firstRow = #() lastRow = #() firstRowMinU = 1.0 -- set minU to 1. At least one U value should be less than this. lastRowMinU = 1.0 local numVertices = selectedPoly.GetNumVertices() --format "numVertices = %\n" numVertices to:debug for i = 1 to numVertices do ( pos = getVertexPos i if i == 1 then ( minDimensions = copy pos maxDimensions = copy pos ) minDimensions.x = minOf minDimensions.x pos.x minDimensions.y = minOf minDimensions.y pos.y minDimensions.z = minOf minDimensions.z pos.z maxDimensions.x = maxOf maxDimensions.x pos.x maxDimensions.y = maxOf maxDimensions.y pos.y maxDimensions.z = maxOf maxDimensions.z pos.z numEdges = selectedPoly.GetVertexEdgeCount i -- find the pole vertices by their position in the UV map. local uvPos = getUVpos i if uvPos.y < POLE_V_LIMIT[1] or uvPos.y > POLE_V_LIMIT[2] then ( if uvPos.y > 0.5 then ( if uvPos.x < firstRowMinU then insertItem i firstRow 1 else append firstRow i firstRowMinU = minOf firstRowMinU uvPos.x ) else ( if uvPos.x < lastRowMinU then insertItem i lastRow 1 else append lastRow i lastRowMinU = minOf lastRowMinU uvPos.x ) ) ) maxScale = maxOf (maxDimensions.x - minDimensions.x) (maxDimensions.y - minDimensions.y) maxScale = maxOf maxScale (maxDimensions.z - minDimensions.z) calcPrimSizeToKeepProportions() ) -- pass1 fn getOtherSideOf aVertex edgeNum = ( local edgeId = selectedPoly.GetVertexEdge aVertex edgeNum anotherVertex = selectedPoly.GetEdgeVertex edgeId 1 if anotherVertex == aVertex then anotherVertex = selectedPoly.GetEdgeVertex edgeId 2 anotherVertex ) -- getOtherSideOf fn connectsToPreviousRow aVertex = ( local numEdges = selectedPoly.GetVertexEdgeCount aVertex local isConnected = false for i = 1 to numEdges do ( otherVertex = getOtherSideOf aVertex i isConnected = findItem prevRow otherVertex != 0 if isConnected then exit ) isConnected ) -- connectsToPreviousRow fn isNotInPreviousTwoRows aVertex = ( (findItem prevRow aVertex == 0) and (findItem currRow aVertex == 0) ) -- isNotInPreviousTwoRows fn getFirstVertexInNextRow = ( local currRowStart = currRow[1] local numEdges = selectedPoly.GetVertexEdgeCount currRowStart local firstVertex = -1 for i = 1 to numEdges do ( aVertex = getOtherSideOf currRowStart i if isNotInPreviousTwoRows aVertex then ( firstVertex = aVertex exit ) ) firstVertex ) -- getFirstVertexInNextRow fn getNextVertexInRow curVertex = ( local curUV = getUVpos curVertex local curNumEdges = selectedPoly.GetVertexEdgeCount curVertex local nextVertex = -1 for i = 1 to curNumEdges do ( aVertex = getOtherSideOf curVertex i local aUV = getUVpos aVertex --the following conditionals might be more complicated than necessary. --the first IF was the original algorithm. --the nested IF is a revised algorithm to use the UV map for guidance. --it might be that the first IF can be simplified, but I haven't gotten around to it. if connectsToPreviousRow aVertex and isNotInPreviousTwoRows aVertex then ( if aUV.x > curUV.x and aUV.x - curUV.x < 0.5 then ( nextVertex = aVertex exit ) ) ) --if nextVertex is -1, then we might have a problem --except that we always call getNextVertexInRow one more time than we need to --so it is -1 at the end of every row. But we don't use that value. nextVertex ) -- getNextVertexInRow fn setInBitmap bitmapPos color = ( setPixels sculptBitmap bitMapPos #(color) ) fn putInBitmap vertId meshRow meshCol = ( -- bitmap: x and y start at 0. x is horizontal, y is vertical -- mesh: row and col start at 0. row is vertical, col is horizontal -- -- middle meshRows are put in the 64x64 bitmap at odd Y values 1..61 (31 rows total) -- X values for middle rows are even (32 columns total) -- so they go [0,1], [2,1]...[62,1] -- then [0,3], [2,3]...[62,3] .... [0,61], [2,61]...[62,61] -- -- Poles are defined at [32,0] and [32,63]. Giving us 33 rows with data in a 64x64 bitmap -- even Y values 2..62 are unused. (31 rows unused) -- in the following line *bitmapTo64Ratio compensates for the size of bigger bitmaps -- while (meshRow*2) + 1 ensures that the rows are odd. -- and meshCol*2 ensures that the columns are even local bitmapPos = [(meshCol * 2) * bitmapTo64RatioX, ((meshRow * 2) + 1) * bitmapTo64RatioY] if HORIZ_FLIP then ( bitmapPos.x = BITMAP_X - bitmapPos.x - 2 * bitmapTo64RatioX ) local vertPos = getVertexPos vertId local vertColor = vertexToColor vertPos -- shift is a hack to reduce JPEG compression artifacts local shift = 0 if bitmapTo64RatioX > 1 then shift = -1 -- the loops allow us to write more than one pixel for each mesh vertex. -- for example, if the mesh is 32x32 and the bitmap is 64x64, then each vertex in -- the mesh is represented by 4 pixels in the bitmap for xOffset = 0 + shift to bitmapTo64RatioX * 2 - 1 do ( for yOffset = 0 + shift to bitmapTo64RatioY * 2 - 1 do ( local x = bitmapPos.x + xOffset local y = bitmapPos.y + yOffset if x >= 0 then setInBitmap [x, y] vertColor ) ) ) -- putInBitmap fn processEndPoints vertexArray yStart = ( local vertId = vertexArray[1] -- arbitrarily choose the first one, they're all the same local vertPos = getVertexPos vertId local vertColor = vertexToColor vertPos for x = 0 to BITMAP_X - 1 do ( for yOffset = 0 to bitmapTo64RatioY - 1 do ( setInBitmap [x, yStart + yOffset] vertColor ) ) ) -- processEndPoints fn refreshDisplay = ( SculptGenMax_CanvasRollout.theCanvas.bitmap = sculptBitmap ) fn pass2 = ( -- in Pass 2, we start with an endpoint and traverse the mesh one row at a time. -- row and column numbers start at 0 -- if the bitmap resolution is greater than the mesh resolution, then we just copy the -- a mesh point into surrounding pixels. Thus, each point in a 32x32 mesh will -- be represented by 4 identical pixels in a 64x64 bitmap. local currVertex processEndPoints firstRow 0 -- put the first row in the bitmap processEndPoints lastRow (BITMAP_Y - bitmapTo64RatioY) -- put the last row in the bitmap currRow = firstRow prevRow = #() for rowNum = 0 to MESH_ROWS-1 do -- loop through the middle rows ( currVertex = getFirstVertexInNextRow() prevRow = currRow currRow = #() for colNum = 0 to MESH_COLS-1 do ( putInBitmap currVertex rowNum colNum append currRow currVertex currVertex = getNextVertexInRow currVertex ) refreshDisplay() ) ) -- pass2 fn updatePrimSizeTextBoxes = ( SculptGenMax_CanvasRollout.prim_size_x.text = propPrimSize.x as string SculptGenMax_CanvasRollout.prim_size_y.text = propPrimSize.y as string SculptGenMax_CanvasRollout.prim_size_z.text = propPrimSize.z as string ) fn makeHorizStripeTexture = ( -- this function creates a texture map with a green horizontal stripe across the top and -- then alternating black and red horizontal stripes. -- useful for debugging UV mapping issues. local BITMAP_XY = 512 local XY_ADJUST = BITMAP_XY/64 local texture = bitmap BITMAP_XY BITMAP_XY color:white -- generate the bitmap here local red = #( [255,0,0] ) local black = #( [0,0,0] ) local green = #( [0,255,0] ) for x = 0 to BITMAP_XY - 1 do ( for yOffset = 0 to XY_ADJUST * 2 - 1 do ( setPixels texture [x, 0 + yOffset] green ) local y = 1 local color = black while y <= 61 do ( for yOffset = 0 to XY_ADJUST * 2 - 1 do ( setPixels texture [x, (y + 1) * XY_ADJUST + yOffset] color ) y += 2 if color == black then color = red else color = black ) ) -- makeHorizStripeTexture local theSaveName = getSaveFileName types:"Targa (*.tga)|*.tga|BMP (*.bmp)|*.bmp|JPEG (*.jpg)|*.jpg" if theSaveName != undefined do ( texture.filename = theSaveName save texture ) ) on generate picked do ( SculptGenMax_CanvasRollout.theCanvas.bitmap = blankBitmap sculptBitmap = copy blankBitmap -- this builds a test texture to show how uv mapping works. -- upload texture and apply it as the diffuse texture of the sculptie -- the generated texture has green on the top line and then alternates red and black. --makeHorizStripeTexture() anObject = snapshot selection[1] selectedPoly = anObject try ( convertTo selectedPoly (Editable_Poly) --convert to Editable_Poly pass1() updatePrimSizeTextBoxes() pass2() ) catch ( -- if anything goes wrong, make sure that we delete the temp copy of the -- selection. If we don't, it will show up in the scene. delete selectedPoly throw() ) delete selectedPoly refreshDisplay() ) --generate picked on save_as picked do ( theSaveName = getSaveFileName types:"Targa (*.tga)|*.tga|BMP (*.bmp)|*.bmp|JPEG (*.jpg)|*.jpg" if theSaveName != undefined do ( sculptBitmap.filename = theSaveName save sculptBitmap ) ) --save_as picked on about_tool picked do ( local msgString = PROG_NAME + "\nVersion " + PROG_VERSION + "\nCopyright \xa9 2007 Shack Dougall" messagebox msgString title:"About..." ) on quit_tool picked do destroyDialog SculptGenMax_CanvasRollout ) --rcMenu CanvasMenu rollout SculptGenMax_CanvasRollout PROG_NAME ( bitmap theCanvas pos:[0,0] width:BITMAP_X height:BITMAP_Y bitmap:sculptBitmap listbox resolution items:#("64x64", "128x128", "256x256") selection: default_resolution pos:[MAX_BITMAP+5,0] width:90 height:3 checkbox keep_props "Proportional" checked: default_keep_proportions align: #right edittext prim_size_x "X:" fieldWidth:10 width:80 labelOnTop:false enabled:false align: #right edittext prim_size_y "Y:" fieldWidth:10 width:80 labelOnTop:false enabled:false align: #right edittext prim_size_z "Z:" fieldWidth:10 width:80 labelOnTop:false enabled:false align: #right dropdownlist map_channel "UV Map Channel" items:#("Channel 1","2","3","4","5","6","7","8","9") selection:default_uv_map_channel height:11 width:80 align: #right on resolution selected index do ( BITMAP_X = BITMAP_Y = 2 ^ (index + 5) bitmapTo64RatioX = BITMAP_X/64 bitmapTo64RatioY = BITMAP_Y/64 sculptBitmap = bitmap BITMAP_X BITMAP_Y color:white -- generate the bitmap here blankBitmap = bitmap BITMAP_X BITMAP_Y color:white -- used to reset the dialog theCanvas.width = BITMAP_X theCanvas.height = BITMAP_Y theCanvas.bitmap = copy blankBitmap ) -- resolution selected on keep_props changed theValue do ( keep_proportions = theValue prim_size_x.enabled = not keep_proportions prim_size_y.enabled = not keep_proportions prim_size_z.enabled = not keep_proportions ) -- keep_props changed on map_channel selected theMap do ( UV_MAP_CHANNEL = theMap ) ) --rollout createDialog SculptGenMax_CanvasRollout (MAX_BITMAP+100) (MAX_BITMAP+30) menu:CanvasMenu ) --macroscript