View Full Version : Grabbing The Material Preview


LoneRobot
12 December 2014, 10:44 PM
Hey All,

http://lonerobot.net/wp-content/uploads/2014/12/matPrevThumbs.png

I just wrote a new post detailing my method for capturing the preview from the material editor. I thought I'd share as it's something I've wanted to do for years but have not been able to figure out until now. What it does is allow high-res thumbnails to be generated to file from the material editor window. It's also disgustingly hacky, so avert your eyes if you are of a nervous scripting disposition. :surprised

I wrote it so that I can begin writing a shader database system for work, but since I wrote this part in my spare time, I can share it with you peeps. Hopefully it will be useful for any similar material based asset tracking scripts you might want to write.

http://lonerobot.net/?p=1918

DaveWortley
12 December 2014, 12:31 AM
I've got another slightly hacky way of doing it.

Install Kees's Helium plugin, and you can use the function to get the shader ball as a bitmap value. It's not quite as good as the real preview but it's a lot less lines of code than yours :P

48design
12 December 2014, 10:00 AM
Wow, great! I don't know why Autodesk doesn't give access to this part directly. It would be really great to have the option to have our own material preview window inside our scripts/plugins...

Thank you!

LoneRobot
12 December 2014, 11:07 AM
It's not quite as good as the real preview but it's a lot less lines of code than yours :P

Ha, that's because someone else did the hard work for you, Dave :-D

Good to know that helium can do this too though - thanks.

Klvnk
12 December 2014, 11:42 AM
this will do it

//********************************************************************************************
// MtlPreviewToBitmap

def_visible_primitive(MtlPreviewToBitmap, "MtlPreviewToBitmap");

Value* MtlPreviewToBitmap_cf(Value** arg_list, int count)
{
check_arg_count(MtlPreviewToBitmap, 2, count);

MtlBase* mb = arg_list[0]->to_mtlbase();
int sz = arg_list[1]->to_int();

PStamp *ps = mb->CreatePStamp(sz,1);
if(ps)
{
int d = PSDIM(sz);
int scanw = ByteWidth(d);
int nb = scanw*d;

UBYTE *buf = new UBYTE[nb + sizeof(BITMAPINFOHEADER)];
if(!buf)
&undefined;

// get the image data

ps->GetImage(&buf[sizeof(BITMAPINFOHEADER)]);

// and create the header info

BITMAPINFOHEADER* bih = (BITMAPINFOHEADER*)buf;
bih->biSize = sizeof(BITMAPINFOHEADER);
bih->biWidth = d;
bih->biHeight = d;
bih->biPlanes = 1;
bih->biBitCount = 24;
bih->biCompression = BI_RGB;
bih->biSizeImage = 0;

Bitmap *map = TheManager->Create((PBITMAPINFO)buf);
delete [] buf;

if(map)
{
one_typed_value_local(MAXBitMap* mbm);
vl.mbm = new MAXBitMap();
vl.mbm->bi.CopyImageInfo(&map->Storage()->bi);
vl.mbm->bi.SetFirstFrame(0);
vl.mbm->bi.SetLastFrame(0);
vl.mbm->bi.SetName("");
vl.mbm->bi.SetDevice("");
if(vl.mbm->bi.Type() > BMM_TRUE_64)
vl.mbm->bi.SetType(BMM_TRUE_64);
vl.mbm->bm = map;
return_value(vl.mbm);
}
}
return &undefined;
}

sz = 0 -> 32*32
sz = 1-> 88*88
sz > 1 -> 24*24

display (MtlPreviewToBitmap (getMeditMaterial 1) 0)

edited to remove memcpy

stigatle
12 December 2014, 12:16 PM
Is it possible to do the other way around?
Meaning - sending in a custom bitmap as a 'material preview'?

The reason why I ask is because in a exporter I've been working on for a long time has it's own 'preview tab' in the material, which then shows the external rendered preview image.
It would be very nice to send that image back into the material slot directly instead.

The exporter calls on a external renderer, that's why it works with the current setup - we render a preview scene externally, save to a file named the same as material, then when you open that material in the editor it just shows that image.

Klvnk
12 December 2014, 12:21 PM
yes but I think it would need a custom mtl plugin which overides the mtlbase pstamp routines.

it might be possible without the use of a custom mtl, as the PStamp object has a setimage method.

denisT
12 December 2014, 02:34 PM
this will do it
display (MtlPreviewToBitmap (getMeditMaterial 1) 0)
very nice! could you post it on "useful mxs extensions" thread, please?

PolyTools3D
12 December 2014, 05:55 PM
Thank you guys for sharing this.

@Pete:
There is a action that opens the preview ("Magnify") window. Perhaps you may want to check it out and see if that could replace the double click hack? actionMan.executeAction 2 "40296"

LoneRobot
12 December 2014, 06:13 PM
Hi Chaps,

thanks for all your input.

Klunk - For those of us not clear about how to approach the SDK, is this method easily usable? My limit is that I've built an SDK maxscript extension before, is this a case of adding this and re-compiling? Many thanks for your solution

PolyTools3d - Thanks a million, anything to remove some of my hacky steps is awesome, I'll change the code and update the article, I'm sure that will work much better.

Klvnk
12 December 2014, 06:54 PM
the easiest way is to copy and paste into the any of the files of the mxsagni project in the samples and recompile (as release) it and replace the mxsagni.dlx in stdplugins with the newly compiled version.

try2script
12 December 2014, 07:50 PM
Klvnk (http://forums.cgsociety.org/member.php?u=544483). could you make a tutorial how to do this please? When I look to Useful mxs sdk extension functions (http://forums.cgsociety.org/showthread.php?f=98&t=1089363) I understand that for some people it is useful, but unfortunately not for me, because I don't know how to use them :(

Or, at least, point us to a "short" tutorial.

DaveWortley
12 December 2014, 10:47 PM
Yes please Klunk, that would be an awesome tutorial. I'd love to use that function.

denisT
12 December 2014, 11:58 PM
guys! using MAX sdk is the power. (Klunk can confirm it :))

denisT
12 December 2014, 12:01 AM
I'd love to use that function. almost all functions in the "useful maxscript extentions" thread are really-really useful.

k4noe
12 December 2014, 06:36 AM
thanks Klvnk, would it be possible to add "cleaan way" or if possible on the fly compile like in C# code ?

Klvnk
12 December 2014, 02:14 PM
I'm sure it's possible but I'm probably not the man for that sort of thing c# is not my bag ! :)

LoneRobot
12 December 2014, 02:35 PM
Thank you guys for sharing this.

@Pete:
There is a action that opens the preview ("Magnify") window. Perhaps you may want to check it out and see if that could replace the double click hack? actionMan.executeAction 2 "40296"

Can anyone verify if this works? I've tried this call in 2013, 2014 and 2015 and it returns false with no material window magnified.

I'd +1 for a SDK heads up if anyone was kind enough to do it.

LoneRobot
12 December 2014, 02:42 PM
Can anyone verify if this works? I've tried this call in 2013, 2014 and 2015 and it returns false with no material window magnified.


Ack, my bad - its just a focus issue, you need to call MatEditor.Open() before you run the actionman item.

denisT
12 December 2014, 02:44 PM
Can anyone verify if this works?
it's a Material Editor action. it means the editor has to be in focus when you execute this action.

edited
oops. you already answered your question

LoneRobot
12 December 2014, 02:50 PM
Ya, thanks Denis, reckon I should ban myself from trying to think at weekends.

PolyTools3D
12 December 2014, 09:06 PM
Opening the preview window seems to be a little buggy, especially if you already have some preview windows opened.

I thought perhaps using just one Material Slot and changing it could be useful too, for example if you have a scene with materials that are not currently opened in the Editor.

I haven't tested if it really captures the material preview or if it works with different Renderers. In theory I believe it should work.

The code below does not use the DialogMonitorOPS. It is just some testing thing and so it is a little messy. Also it is missing the C# functions for capturing the screen.

The flashing Editor is annoying, but I think it could be hidden with a bit of C# or moved out of screen. (

fn GrabPreviewThumbnail mHWND =
(
-- Nothing here, just print the material name
print (UIAccessor.GetWindowText mHWND)
)

fn GetMaterialEditorHWNDs mClosePreviews:true =
(
children = windows.getChildrenHWND 0

editorHWND = undefined
previewHWND = undefined

for j in children do
(
hwnd = j[1]
class = j[4]

if class == "#32770" do
(
DLL = UIAccessor.GetWindowDllFileName hwnd
DLL = filenamefromPath DLL

if DLL == "res1.dll" do
(
if (UIAccessor.GetChildWindows hwnd).count == 3 then
(
if mClosePreviews do UIAccessor.CloseDialog hwnd
previewHWND = hwnd
)else(
editorHWND = hwnd
)
)
)
)

return #(editorHWND, previewHWND)

)

fn BuildMaterialsPreviews mType =
(
case mType of
(
1: (materials = for j in scenematerials collect j)
2: (materials = for j in meditmaterials collect j)
default: return ()
)

materialEditor = MatEditor.mode
MatEditor.mode = #basic

MatEditor.Open()

GetMaterialEditorHWNDs mClosePreviews:true

medit.SetActiveMtlSlot 1 true
windows.processPostedMessages()

materialSlot1 = meditmaterials[1]
actionMan.executeAction 2 "40296"

HWNDs = GetMaterialEditorHWNDs mClosePreviews:false
previewHWND = HWNDs[2]

for j in materials do
(
meditmaterials[1] = j
windows.processPostedMessages()

-- Grab Image here
GrabPreviewThumbnail previewHWND
)

UIAccessor.CloseDialog previewHWND
meditmaterials[1] = materialSlot1
MatEditor.mode = materialEditor
)

BuildMaterialsPreviews 1

)

denisT
12 December 2014, 10:51 PM
recent max versions have getWindowPos method. the preview window pops up on the top. so we can use .net screen grab methods instead of print HDC

so today with recent versions of max we can do everything without extra pre-compiled (or on-the-fly) .net assemblies

PolyTools3D
12 December 2014, 11:23 PM
recent max versions have getWindowPos method. the preview window pops up on the top. so we can use .net screen grab methods instead of print HDC
Yes, I thought of capturing the screen too, but what would happen if the user switches to another application in the process?

denisT
12 December 2014, 11:51 PM
Yes, I thought of capturing the screen too, but what would happen if the user switches to another application in the process?
what is the user doing? grabbing material previews. why does he wish to switch to anything else?
the same way i can ask what would happen if a user plugs his computer off? the proofing a tool from pets and blondes is not a modern technique. ;)

PolyTools3D
12 December 2014, 12:49 AM
what is the user doing? grabbing material previews. why does he wish to switch to anything else?Very logic, very lineal... but...
Life is not so logic neither so lineal. Otherwise software development would be a piece of cake.:)

k4noe
12 December 2014, 07:46 AM
my thought exactly ....thats why smart people invent smart proof.....

denisT
12 December 2014, 08:34 AM
my thought exactly ....thats why smart people invent smart proof.....
could you post any sample of your smart proof?

denisT
12 December 2014, 09:01 AM
pot.transform.controller = prs()
look at your own code...
http://forums.cgsociety.org/showpost.php?p=7889075&postcount=6

it's only one simple assignment. but i can give you at least four different cases where it can't work. does it need "foolproof".
can anyone show 'smart proof' for this line? ;)

denisT
12 December 2014, 09:07 AM
t_bmp = try (openbitmap arrImg[i]) catch()
bmp_w = t_bmp.width
bmp_h = t_bmp.height

it's another sample of your code... which shows how a "proofing" converts to mistake.

haavard
12 December 2014, 01:52 PM
I tried to follow Klvnk's lead and Im almost there. The PStamp object has an Image property which is a byte array with size of 3 * Height * Width of the image, all good in the hood i thought, but I cant convert the array into a picture. I looked at the array in visual studio and it looks fine to me, don't know what's wrong.

fn MtlPreview mtlIndex size =
(
glob = (dotnetClass "Autodesk.Max.GlobalInterface").Instance
if glob != undefined then
(
mtl = glob.COREInterface14.GetMtlSlot mtlIndex
if mtl != undefined then
(
pStamp = mtl.CreatePStamp size true
bytes = dotnet.ValueToDotnetObject pStamp.Image (dotnetClass "system.byte[]")
imgConverter = dotnetObject "System.Drawing.ImageConverter"
img = imgConverter.ConvertFrom bytes

/*
ms = dotNetObject "System.IO.MemoryStream" bytes
bm = dotNetObject "System.Drawing.Bitmap" ms
ms.dispose()
*/

)
)
)

MtlPreview 0 (dotnetclass "Autodesk.Max.PostageStampSize").LargeSize

denisT
12 December 2014, 03:19 PM
http://stackoverflow.com/questions/6782489/create-bitmap-from-a-byte-array-of-pixel-data

look this solution... it's probably how i would do.

as i know there is no built-in .net solution for creating an image from raw pixel data (array of bytes)

haavard
12 December 2014, 05:04 PM
You're right, denisT, thanks!

This works:
(
fn MtlPreview mtlIndex size =
(
glob = (dotnetClass "Autodesk.Max.GlobalInterface").Instance
if glob != undefined then
(
bm = undefined
mtl = glob.COREInterface14.GetMtlSlot mtlIndex
if mtl != undefined then
(
pStamp = mtl.CreatePStamp size true
bytes = dotnet.ValueToDotnetObject pStamp.Image (dotnetClass "system.byte[]")
bm = dotnetObject "System.Drawing.Bitmap" pStamp.Width pStamp.Height

bmData = bm.LockBits \
(dotnetObject "System.Drawing.Rectangle" 0 0 pStamp.Width pStamp.Height) \
(dotnetClass "System.Drawing.Imaging.ImageLockMode").WriteOnly \
(dotnetClass "System.Drawing.Imaging.PixelFormat").Format24bppRgb

ptr = dotnetObject "System.IntPtr" bmData.Scan0

(dotnetClass "System.Runtime.Interopservices.Marshal").Copy bytes 0 ptr bytes.Length
bm.UnlockBits(bmData)

pStamp.Dispose()

)
--mtl.Dispose() ?

bm
)
)

MtlPreview 0 (dotnetclass "Autodesk.Max.PostageStampSize").Large
)

Here is the enum for the size argument:
public enum PostageStampSize
{
Small = 0,
Large = 1,
Tiny = 2,
TinySize = 24,
SmallSize = 32,
LargeSize = 88,
}

denisT
12 December 2014, 05:39 PM
if you LockBits the copying will be much faster.

haavard
12 December 2014, 05:55 PM
I'm not following you here. I am using the lockbits-method. It is where I defined the format of the image as well.

PolyTools3D
12 December 2014, 06:16 PM
Thank you håvard!
Images are flipped in Y axis. You may want to add: bm.RotateFlip (dotNetClass "System.Drawing.RotateFlipType").RotateNoneFlipY

denisT
12 December 2014, 07:47 PM
I'm not following you here. I am using the lockbits-method. It is where I defined the format of the image as well.
ah... sorry. i missed this part in your code. my bad

PolyTools3D
12 December 2014, 01:04 AM
Here is a similar function but returns a MaxScript bitmap. The material index is 0 based.
The performance is almost the same as using LockBits() with a .Net bitmap. (
fn GetMaterialThumbnail mMatIdx mSize =
(
iGlobal = (dotnetClass "Autodesk.Max.GlobalInterface").Instance
if iGlobal != undefined do
(
iMaterial = iGlobal.CoreInterface.GetMtlSlot mMatIdx

if iMaterial != undefined do
(
pStamp = iMaterial.CreatePStamp mSize true
bytes = pStamp.Image
size = pStamp.Width

bm = bitmap size size

step = size*3
for y = 1 to bytes.count by step do
(
row = for x = y to (y+step-1) by 3 collect [bytes[x+2], bytes[x+1], bytes[x]]
setpixels bm [0, size-=1] row
)
pStamp.Dispose()
iMaterial.Dispose()
)
)
return bm
)

thumbnail = GetMaterialThumbnail 0 (dotnetclass "Autodesk.Max.PostageStampSize").Large
--display thumbnail
)

denisT
12 December 2014, 08:22 AM
it's only because the image is small. in general the difference is huge. after i knew about locking bits my "bitmap" life changed :)

haavard
12 December 2014, 09:24 AM
Images are flipped in Y axis.

Haha, did not notice since all my materials were gray anyway :)

PolyTools3D
12 December 2014, 11:48 AM
it's only because the image is small. in general the difference is huge. after i knew about locking bits my "bitmap" life changed :)Working with a "locked" bitmap is generally much faster than the common pixel operations, but that is not always the case. Many Bitmap and Graphics operations are just as fast as implementing them with LockBits().

And of course, the larger the image the bigger the difference.

PolyTools3D
12 December 2014, 07:42 PM
A last function that will create an array of materials either from the Editor or the Scene. (

/*
MAX 2013+
MakeMaterialsThumbnails()

Returns an array with materials names, classes and thumbnails.
The materials can be from the Material Editor or from the Scene.
#( #(name, class, bitmap) )
#( #("01 - Default", "Standard", BitMap:) )

MakeMaterialsThumbnails type:<name> size:<int>
type: #scene, #editor
size: 0=24x24, 1=32x32, 2=88x88
*/

fn MakeMaterialsThumbnails type:#scene size:2 =
(
iGlobal = (dotnetClass "Autodesk.Max.GlobalInterface").Instance
IntPtr = dotnetClass "System.IntPtr"

case type of
(
#scene:
(
sceneMtls = iGlobal.CoreInterface.SceneMtls
materials = for j = 0 to sceneMtl.NumSubs-1 collect sceneMtls.Item[dotnetObject IntPtr j]
)
#editor: materials = for j = 0 to 23 collect iGlobal.CoreInterface.GetMtlSlot j
default: return messagebox "Invalid Type"
)

case size of
(
0: pStampSize = (dotnetclass "Autodesk.Max.PostageStampSize").Tiny -- 24x24
1: pStampSize = (dotnetclass "Autodesk.Max.PostageStampSize").Small -- 32x32
2: pStampSize = (dotnetclass "Autodesk.Max.PostageStampSize").Large -- 88x88
default: return messagebox "Invalid Size"
)

result = #()
if iGlobal != undefined do
(
for mtl in materials do
(
pStamp = mtl.CreatePStamp pStampSize true
bytes = pStamp.Image
size = pStamp.Width

bm = bitmap size size

step = size*3
for y = 1 to bytes.count by step do
(
row = for x = y to (y+step-1) by 3 collect [bytes[x+2], bytes[x+1], bytes[x]]
setpixels bm [0, size-=1] row
)

append result #(mtl.Name, mtl.ClassName, bm)

pStamp.Dispose()
mtl.Dispose()
)
)
return result
)

mtls = MakeMaterialsThumbnails type:#editor size:2

)

LoneRobot
12 December 2014, 11:32 PM
Thank you Jorge, Haarvard, Denis and Klunk for your input, it is great stuff. :bowdown:
This solution is much. much better.

try2script
12 December 2014, 01:29 AM
Thank you Jorge, Haarvard, Denis, Klunk and LoneRobot!
Jorge, is it possible to make your last script for 2012 as well?

PolyTools3D
12 December 2014, 03:36 AM
Thank you Jorge, Haarvard, Denis, Klunk and LoneRobot!
Jorge, is it possible to make your last script for 2012 as well?The .Net Wrapper was released with Max 2013. It was also available for Max 2012 with a Subscription Pack but I don't know if these features were implemented in that release.


1