SculptGenMax
Revision as of 13:15, 18 December 2007 by Shack Dougall (talk | contribs)
Description
SculptGenMax is a lightweight script that allows users to create sculptie prims in 3ds Max and export them to Second Life.
Current Version
1.0 RC 1 — released 2007/12/18
Features
* Roundtrip workflow from 3ds Max to Second Life and back * Load sculptmaps from disk to import sculpties into 3ds Max * Create sculpties in 3ds Max and export them to Second Life * All sculptmap topologies supported: sphere, torus, plane, and cylinder * Editable Poly, Editable Mesh, and NURBS supported * Preserves UV Mapping — Baked textures work seamlessly
License
* SculptGenMax is licensed under GPL3 * Copywrite 2007 Shack Dougall * GPL FAQ * Quick Guide to GPL3
File:Http://files.liferain.com/downloads/wp-content/uploads/2007/12/sculptgenmax-in-3dsmax.jpg
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