Vector_Map TextureMap and MAXScript
Introduction
The Vector_Map TextureMap available in 3ds Max 2014 and higher provides support for scalable vector graphics file formats including the standard
SVG XML open standard developed by the World Wide Web Consortium. It also supports
compressed SVG in .SVGZ format, Adobe Illustrator .AI files, Adobe Portable Document
Format (.PDF) files and Autodesk AutoCAD Pattern (.PAT) files.
In addition to the exposure of the Vector_Map TextureMap object to MAXScript documented here, the 3ds Max implementation provides exclusive support for MAXScript functions and expressions
inside the actual SVG XML file, allowing for a much tighter integration of vector
graphics with the actual scene content. This includes the creation of dynamic vector
graphics reacting the scene animation or depending on scene objects properties, text
rendering and much more.
This topic discusses the creation of SVG files using MAXScript, the inclusion of MAXScript
commands inside the SVG XML source files, and provides some examples demonstrating
the power of this feature.
Writing SVG Files
Basic Format Structure
The SVG file is a XML text file. Its content is enclosed in <svg> tags, which identifies
the content as Scalable Vector Graphics definition.
Inside the body of the SVG file, one or more entities can be defined, including rectangles,
circles, ellipses, lines, polylines, polygons, images and texts.
The outside color (stroke color) and inside color (fill color) can be defined as hexadecimal
values. Patterns and Gradients can also be used instead of solid colors.
Various properties specific for each entity can be defined, otherwise they will be
assumed to have default values.
Entities can be transformed and grouped for convenience.
SIMPLEST EXAMPLE:
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="255" y="255" height="512" width="512"
style="stroke:#ff8800; fill: #0000aa"/>
</svg>
|
Save this code in a new text file under the name "Rectangle.svg" in your 3ds Max
UserScripts folder.
Then create a Vector_Map TextureMap and load this code as the Vector File. Then render the resulting map into bitmaps with different resolutions - 160x160, 320x320
and 640x320.
You can do this manually, or using MAXScript:
|
theSVGMap = VectorMap()
theSVGMap.vectorfile = GetDir #userscripts + "\\rectangle.svg"
for i in #([160,160], [320,320], [640,480]) do
display (renderMap theSVGMap size:i filter:true)
|
Evaluating the above MAXScript will render 3 images with different resolutions from
the same scalable graphics definition.
Here are the resulting images:
|
Let's discuss the content of the SVG file to understand how it works.
The 3ds Max Vector_Map TextureMap assumes that the Canvas the graphics are drawn into
is 1024x1024 units in size.
The origin [0,0] is the upper left corner, just like in MAXScript Bitmap values. The
coordinate of the bottom right corner is [1023,1023]
Thus, when specifying the Position and Size of an entity, we have to assume that the
center of the Canvas is at [511,511]. Specifying the upper left corner of the Rectangle
at x=255 and y=255 and giving it a width and height of 512 units aligns it at the
Canvas' center.
This Canvas size assumption is specific for 3ds Max and does not necessarily apply
to SVG and other vector graphics files coming from other sources. That's why the Vector_Map
provides Cropping controls to extract only a portion of the rasterized bitmap if the
entities were defined in other coordinates.
Applying Transformations
Let's rotate the rectangle that we created at 45 degrees about its center.
TRANSFORMATIONS EXAMPLE:
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="255" y="255" height="512" width="512"
style="stroke:#ff8800; fill: #0000aa"
transform="rotate(45 511 511)" />
</svg>
|
Re-save this code in the same text file under the name "Rectangle.svg" in your 3ds
Max UserScripts folder.
Update the Vector_Map either manually in the Material Editor, or via MAXScript using
theSVGMap.reload()
Render the resulting map into a bitmap with resolutions of 320x320.
|
theSVGMap.reload()
display (renderMap theSVGMap size:[320,320] filter:true)
|
Evaluating this script will render the scalable graphics definition and display the
bitmap.
Here are the resulting image:
Let's also translate the shape 150 units along X before we have rotated it. When multiple transformations are specified, each one modifies the current coordinate
system in the given order:
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="255" y="255" height="512" width="512"
style="stroke:#ff8800; fill: #0000aa"
transform="translate(150) rotate(45 511 511)" />
</svg>
|
Saving the SVG definition and running the same MAXScript as above produces the following:
In the above case, the translation was applied before the rotation, so the coordinate
system was first shifted to the right, then the rotation was performed.
If we were to specify the Translation after the rotation, the rotation will occur
first, then the translation will occur in the already rotated coordinate system.
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="255" y="255" height="512" width="512"
style="stroke:#ff8800; fill: #0000aa"
transform="rotate(45 511 511) translate(150)" />
</svg>
|
In this case, the X axis is pointing at the bottom right corner, and the rectangle
is shifted diagonally 150 units in that direction:
|
Creating Text
TEXT EXAMPLE:
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="255" y="255" height="512" width="512"
transform="rotate(45 511 511)"
style="stroke:#ffff00; fill:#001166">
</rect>
<text x="255" y="255"
transform="rotate(45 511 511) translate(15 280)"
style="stroke:#000000; fill:#008800; font-size:100; font-family:Arial">
MAXScript
</text>
</svg>
|
Save this code in a new text file under the name "RectangleText.svg" in your 3ds
Max UserScripts folder.
Run the following MAXScript.
|
theSVGMap = VectorMap()
theSVGMap.vectorfile = GetDir #userscripts + "\\RectangleText.svg"
display (renderMap theSVGMap size:[320,320] filter:true)
|
Evaluating this script will render the following image:
|
Defining Gradients, Stroke Width And Corner Radii
Instead of filling the shape with a solid color, we could define a named Gradient
and use it in place of the color for the rectangle's fill: parameter.
This Gradient will have the id "LinearGradient" and will interpolate linearly along
the Y axis from a dark blue color with zero opacity to a brighter blue color with
full opacity.
In the Rectangle's definition, we will use the id "LinearGradient" to pass this definition
to the fill: parameter.
We will also set the corner radii to 30 units and the stroke width of both the rectangle
and the text to 3 units.
GRADIENT EXAMPLE:
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="LinearGradient1"
x1="0%" y1="0%"
x2="0%" y2="100%"
spreadMethod="pad">
<stop offset="0%" stop-color="#001166" stop-opacity="0"/>
<stop offset="100%" stop-color="#0055aa" stop-opacity="1"/>
</linearGradient>
</defs>
<rect x="255" y="255" height="512" width="512" rx="30" ry="30"
transform="rotate(45 511 511)"
style="stroke:#ffff00; fill:url(#LinearGradient1); stroke-width: 10">
</rect>
<text x="255" y="255"
transform="rotate(45 511 511) translate(15 280)"
style="stroke:#000000; stroke-width:3; fill:#008800; font-size : 100; font-family:Arial">
MAXScript
</text>
</svg>
|
Save this code in a new text file under the name "RectangleGradientAndText.svg" in
your 3ds Max UserScripts folder.
Run the following MAXScript. We are now rendering into a pre-created bitmap in order
to produce a correct Alpha channel.
|
theSVGMap = VectorMap alphasource:0 --enable Alpha
theSVGMap.vectorfile = GetDir #userscripts + "\\RectangleGradientAndText.svg"
theImage = bitmap 320 320 --create a bitmap with the desired size
renderMap theSVGMap into:theImage filter:true --render into that bitmap
display theImage --display the bitmap
|
Evaluating this script will render the following image, and the alpha channel will
reflect correctly the gradient's varying opacity:
|
Online Documentation
The official website of the SVG format http://www.w3.org/Graphics/SVG/ contains the
current specification.
Several SVG Introductions and Tutorials can be found online.
Generating SVG Files Using MAXScript
Instead of typing the SVG text files by hand, we can employ MAXScript's ability to
write ASCII Text Files to create SVG files procedurally.
For example, let's create a function for outputting a circle with specific radius
and colors at a user-defined position and call that function multiple times using
MAXScript FOR loops.
We will also need a function to convert a MAXScript color value to a Hexadecimal color
value as used by XML and HTML.
NOTE:'Single quotes' can be used in place of "double quotes" inside the SVG file - this
makes the MAXScript output easier since we don't have to escape them as \"
EXAMPLE:
|
(
--The following function converts a MAXScript color value to a Hexadecimal color value
fn ColorToHex col =
(
theComponents = #(bit.intAsHex col.r, bit.intAsHex col.g, bit.intAsHex col.b)
theValue = "#"
for i in theComponents do
theValue += (if i.count == 1 then "0" else "") + i
theValue
)
--The following function outputs a basic circle definition to a given SVG file:
fn createSVGCircle SVGFile pos radius:100 stroke:yellow fill:red thick:1 =
(
format "<circle cx='%' cy='%' r='%' \n" pos.x pos.y radius to:SVGFile
format "\tstyle='stroke:%; fill:%; stroke-width:%' >\n" (ColorToHex stroke) (ColorToHex fill) thick to:SVGFile
format "</circle>\n" to:SVGFile
)
theSVGFilename = GetDir #userScripts + "\\Circles.svg" --Define an output file name
theSVGFile = createFile theSVGFilename --Create a text file
format "<svg xmlns=\"http://www.w3.org/2000/svg\"\n" to:theSVGfile --Output the SVG header lines
format "\t\txmlns:xlink=\"http://www.w3.org/1999/xlink\">\n" to:theSVGfile
theStep = 100 --This is the step in pixels along both X and Y
for y = 0 to 1024 by theStep do --Loop along Y and X with the given Step
for x = 0 to 1024 by theStep do --and create circles with position and fill color based on the loops:
createSVGCircle theSVGFile [x,y] radius:(theStep/2) stroke:white fill:(color (x/4) (y/4) 100) thick:5
format "</svg>\n" to:theSVGfile --End of the SVG body definition
close theSVGfile --Close the file
theSVGMap = VectorMap() --Create a new Vector_Map instance
theSVGMap.vectorFile = theSVGFilename --Assign the output file to the new map
display (renderMap theSVGMap size:[512,512] filter:true) --Render the map at 512 resolution and display
)
|
Here is the resulting map rendered as image: By just changing theStep variable from 100 to 50, we can easily produce more and smaller
circles:
|
Let's modify the loop further - we can shift every second row by half the step to
the side and reduce the Y loop to use half the step.
We will also use a Stroke Thickness of 2 to produce finer outlines:
|
--Partial Code Fragment - insert in the above script!
theStep = 50
cnt = 0 --introduce a counter variable
for y = 0 to 1024 by theStep/2 do --divide the vertical step by two
(
cnt = 1-cnt --change the counter between 0 and 1 after each row
for x = 0 to 1024 by theStep do --shift the X by half the step multiplied by the counter (0 or 1):
(
createSVGCircle theSVGFile radius:(theStep/2) pos:[x+(theStep/2*cnt),y] stroke:white fill:(color (x/4) (y/4) 100) thick:2
)
)
--End Code Fragment
|
The result looks like scales!
|
Including Static Scene Properties
Since MAXScript can easily read properties of scene objects, we could output scene
geometry (for example spline shapes) to SVG and thus turn the 3ds Max viewports into
a simple SVG Editor!
For example, we can take the Circle drawing function above and implement similar functions
for the other typical shapes including Rectangle, Ellipse etc.
Then, we can run through the 3ds Max scene and when we encounter a matching spline primitive, we can grab its properties
and output to the SVG file.
EXAMPLE:
|
(
fn ColorToHex col =
(
theComponents = #(bit.intAsHex col.r, bit.intAsHex col.g, bit.intAsHex col.b)
theValue = "#"
for i in theComponents do
theValue += (if i.count == 1 then "0" else "") + i
theValue
)
fn getFillColor obj =
(
if classof obj.material == Standard then
obj.material.diffusecolor
else
obj.wirecolor
)
fn getPos obj =
(
[obj.pos.x, -obj.pos.y]
)
fn createSVGCircle SVGFile pos radius:100 stroke:yellow fill:red thick:1 =
(
format "<circle cx='%' cy='%' r='%' \n" pos.x pos.y radius to:SVGFile
format "\tstyle='stroke:%; fill:%; stroke-width:%' >\n" (ColorToHex stroke) (ColorToHex fill) thick to:SVGFile
format "</circle>\n" to:SVGFile
)
fn createSVGEllipse SVGFile pos rx:100 ry:50 stroke:yellow fill:green thick:1 =
(
format "<ellipse cx='%' cy='%' rx='%' ry='%'\n" pos.x pos.y rx ry to:SVGFile
format "\tstyle='stroke:%; fill:%; stroke-width:%' >\n" (ColorToHex stroke) (ColorToHex fill) thick to:SVGFile
format "</ellipse>\n" to:SVGFile
)
fn createSVGRect SVGFile pos w:100 h:100 rx:0 stroke:yellow fill:blue thick:1 =
(
format "<rect x='%' y='%' width='%' height='%' rx='%' ry='%'\n" pos.x pos.y w h rx rx to:SVGFile
format "\tstyle='stroke:%; fill:%; stroke-width:%' >\n" (ColorToHex stroke) (ColorToHex fill) thick to:SVGFile
format "</rect>\n" to:SVGFile
)
fn createSVGText SVGFile pos textString:"SVG" fontSize:100 stroke:green fill:orange thick:1 =
(
format "<text x='%' y='%'\n" pos.x pos.y to:SVGFile
format "\tstyle='font-size:%; font-family:Arial; stroke:%; fill:%; stroke-width:%' >\n" fontSize (ColorToHex stroke) (ColorToHex fill) thick to:SVGFile
format "%</text>\n" textString to:SVGFile
)
theSVGFilename = GetDir #userScripts + "\\MaxShapes.svg"
theSVGFile = createFile theSVGFilename
format "<svg xmlns=\"http://www.w3.org/2000/svg\"\n" to:theSVGfile
format "\t\txmlns:xlink=\"http://www.w3.org/1999/xlink\">\n" to:theSVGfile
for obj in objects do
(
local o = obj.baseobject
if superclassof o == Shape do
(
case classof o of
(
Circle: createSVGCircle theSVGFile [obj.pos.x,-obj.pos.y] radius:o.radius stroke:obj.wirecolor fill:(getFillColor obj) thick:o.thickness
Ellipse: createSVGEllipse theSVGFile [obj.pos.x,-obj.pos.y] rx:(o.width/2) ry:(o.length/2) stroke:obj.wirecolor fill:(getFillColor obj) thick:o.thickness
Rectangle: createSVGRect theSVGFile [obj.min.x,-obj.max.y] w:o.width h:o.length rx:o.cornerRadius stroke:obj.wirecolor fill:(getFillColor obj) thick:o.thickness
Text: createSVGText theSVGFile [obj.min.x,-obj.min.y] textString:o.text fontSize:o.size stroke:obj.wirecolor fill:(getFillColor obj) thick:o.thickness
)
)
)
format "</svg>\n" to:theSVGfile
close theSVGfile
theSVGMap = VectorMap()
theSVGMap.vectorFile = theSVGFilename
display (renderMap theSVGMap size:[512,512] filter:true)
)
|
Here is a screenshot of a scene containing a green Circle, a dark red Rectangle, a
purple Ellipse and an orange Text shape.
The Circle has a Radius of 100 and no material or modifiers. The Rectangle is 300x200 units and has Corner Radius of 50; its Renderable property
checked, has a Thickness of 5 and a Standard Material with blue diffuse color assigned.
The Ellipse with size 200x150 has its Thickness set to 10, a MeshSelect modifier on
the stack, and has a yellow Standard Material assigned.
The Text has no material and a MeshSelect modifier on the stack. These objects were placed in the bottom right quadrant of the World Origin, so that
their Y coordinates are negative, with the origin in the upper left corner. A green
Point Helper shows the position of the 1023,1023 coordinate.
Running the above script produces the following image: Note that the order of object creation was reflected in the order of shape drawing.
The stroke color was taken from the wireframe (object) color; the fill color was taken
from the Standard Material's diffuse color, or from the wirecolor property when there
was no Material. The Renderable Thickness property was also respected.
Also note that the above script would not take rotations into account and would require
significant modifications to support them due to the difference in coordinate systems
and object pivot points between 3ds Max and SVG.
|
Setting the SVG Definition Without External File
The Vector_Map TextureMap provides a dedicated method .SetSvgString() which can be used to pass the SVG XML definition as a string value without pointing
at a file on disk.
This means that MAXScript could build a complete SVG description in a String variable
or a StringStream and then pass that to the Vector_Map directly.
Using the MXS Token In SVG Files
MAXScript can not only be used to generate SVG files, the 3ds Max Vector_Map TextureMap includes advanced support for MAXScript code embedded in the SVG file itself!
The special mxs token (short for MAXScript) can be used inside the SVG file to include the result
of any MAXScript expression in the XML description, as long as it is a String value.
This opens the door for much deeper integration of SVG maps with MAXScript and 3ds Max.
Not only can SVG objects access 3ds Max scene values dynamically, but the Vector_Map will be updated on time changes, allowing
for animated maps based on animated scene parameters!
ANIMATED SVG EXAMPLE:
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x='0.0' y='800.0' width='1024.0' height='100.0'
style='stroke:#e4d699; fill:#e4d699; stroke-width:1.0' >
</rect>
<ellipse cx='mxs(at time currentTime $Ellipse001.pos.x as string)'
cy='mxs(at time currentTime (-$Ellipse001.pos.y) as string)'
rx='mxs(at time currentTime ($Ellipse001.width/2) as string)'
ry='mxs(at time currentTime ($Ellipse001.length/2) as string)'
style='stroke:#ffffff; fill:#b8e499; stroke-width:3.0' >
</ellipse>
</svg>
|
Saving the above SVG file and loading it with a Vector_Map will produce an animated
texture following the Position and Radius animation of an Ellipse001 object in the
scene.
Assigning that Vector_Map texture to the Diffuse channel of a Standard Material and
assigning it to a Plane produces a dynamic real-time rendering "screen" in the viewport
showing the same animation. In addition, the Map display in the Material Editor will
also render and show the animation:
|
Including MAXScript .MS Files In SVG Files
Instead of using inline MAXScript expressions, the mxs() token inside the SVG file can contain function calls to MAXScript functions defined
in an external .MS file. This makes the most sense in cases where the calculations
are more complex, or the script code has to be modified often and it would be more
convenient to separate the MAXScript calculation from the SVG definition.
The SVG file has to call the MAXScript include() function just once with that external .MS file as argument, and any functions defined
in it will be accessible to the rest of the SVG body to be called via mxs() . These functions must return String values to be included in the SVG stream.
NOTE:The include() call requires double-quotes for the file name. If the include() call has to be inserted within an SVG property value, the SVG value must be in single quotes to co-exist with the MAXScript call.
MAXSCRIPT INCLUDE FILE:
|
fn getUserName =
(
sysinfo.username --this is already a string
)
fn getComputerName =
(
sysinfo.computername --this is already a string
)
fn getNumCPUs =
(
sysinfo.cpucount as string --this is an Integer, so needs conversion to string
)
|
Save the above code to "TextIncludeFn.ms" in your UserScripts folder (getDir #userscripts)
|
SVG FILE:
|
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<text x='100' y='300'
style='stroke:#00FF88; fill:#005588; font-size:70; font-family:Arial'>
Hello mxs(include "TextIncludeFn.ms" getUserName())!
</text>
<text x='100' y='400'
style='stroke:#00FF88; fill:#005588; font-size:70; font-family:Arial'>
Your Computer mxs(getComputerName())
</text>
<text x='100' y='500'
style='stroke:#00FF88; fill:#005588; font-size:70; font-family:Arial'>
has mxs(getNumCPUs()) CPUs
</text>
</svg>
|
Save the above SVG definition as "TextIncludeFn.svg" in the same folder as the .MS
file - (getDir #userscripts)
Note that the include() call is performed just once before the first use of getUserName(),
all further mxs function calls don't need the .MS file re-included!
|
CREATE VECTORMAP TEXTUREMAP:
|
theVMap = VectorMap() --create a vector map
theSVGFileName = GetDir #userScripts + "\\TextIncludeFn.svg" --define the file name of the SVG map source
theVMap.vectorfile = theSVGFileName --assign the SVG file to the map
theTextImage = bitmap 512 512 --create an image for the text
renderMap theVMap into:theTextImage filter:true --render the Vector Map into the image
display theTextImage --display the resulting image
|
RESULT EXAMPLE:
|
|
Using SVG To Render Text To Bitmap
In the following example, we will use the text rendering capabilities of the Vector_Map TextureMap to generate a bitmap containing render time information.
We will create a bitmap using the renderMap() function and overlay it on top of an image produced using the render() function.
EXAMPLE:
|
renderW = 640 --define the render output width
renderH = 480 --define the render output height
textSize = 20 --define the overlay text's height in pixels
theImage = bitmap renderW renderH --define a bitmap for the render output
st = timestamp() --get a timestamp before rendering
render to:theImage vfb:off --render the scene into the existing bitmap
et = timestamp() --get a time stamp after rendering
theTime = "Image Render Time: " +((et-st)/1000.0) as string + " seconds." --define the text to overlay
theVMap = VectorMap() --create a vector map
theSVGFileName = GetDir #userScripts + "\\RenderInfoMap.svg" --define the file name of the SVG map source
theSVGFileHandle = createFile theSVGFileName --create a new text file with that name
format "<svg xmlns=\"http://www.w3.org/2000/svg\"\n" to:theSVGFileHandle --define the svg header
format "\txmlns:xlink=\"http://www.w3.org/1999/xlink\">\n" to:theSVGFileHandle
format "\t<text x='0' y='%'\n" textSize to:theSVGFileHandle --define the text position
format "\t\tstyle='font-size : %;\n" textSize to:theSVGFileHandle --define the text font size
format "\t\tfont-family: Arial; \n" to:theSVGFileHandle --define the text font family
--format "\t\tstroke: #ffffff;\n" to:theSVGFileHandle --define the text outline. Bad Idea, makes it too thick
format "\t\tfill: #ffffff\n" to:theSVGFileHandle --define the fill color of the text as white
format "\t\t'\n" to:theSVGFileHandle --close the font style definition
format "\t>%</text>\n" theTime to:theSVGFileHandle --output the text to display
format "</svg>\n" to:theSVGFileHandle --end the SVG definition
close theSVGFileHandle --close the SVG file
theVMap.vectorfile = theSVGFileName --assign the SVG file to the map
theVMap.alphasource = 0 --enable Alpha output
theTextImage = bitmap 1000 1000 --create an image for the text
renderMap theVMap into:theTextImage filter:true --render the Vector Map into the image --composite the text from the image onto the rendered image
pasteBitmap theTextImage theImage (box2 0 0 1000 (textSize*2.0)) [5,renderH-(textSize*1.5)] type:#composite
display theImage --display the result
|
RESULT EXAMPLE:
|
|
Learn More About SVG and Vector_Map
A three-part tutorial in the How To... section demonstrates the use of the Vector_Map
and the SVG format to render geometry objects as polygons: