cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
Check out the JMP® Marketplace featured Capability Explorer add-in
Choose Language Hide Translation Bar
Craige_Hales
Super User
The Other Kind of Model

Halloween came fast this year. Last year I used this Raspberry Pi project to get a night's worth of entertainment. This year I used JMP and an Arduino Nano. More about the nano, way below.

Since I've been playing with wrapping the earth's picture onto a sphere, I wondered if I could find a similar skull texture to wrap instead. I found something way better: this site has 3D models. This project uses the model the link points to. Thanks to someone using the name printable_models.

 

The attached file is the complete JSL; the snippets in this blog are incomplete and may not quite match.

 

Inside the zip file (use JMP's zip archive to open it):

za = open("e:/Skull_v3_L2.123c1407fc1e-ea5c-4cb9-9072-d28b8aba4c36.zip",zip);
members = za<<dir;

{"Skull_v3_L2.123c1407fc1e-ea5c-4cb9-9072-d28b8aba4c36/",
"Skull_v3_L2.123c1407fc1e-ea5c-4cb9-9072-d28b8aba4c36/Skull.jpg",
"Skull_v3_L2.123c1407fc1e-ea5c-4cb9-9072-d28b8aba4c36/12140_Skull_v3_L2.obj",
"Skull_v3_L2.123c1407fc1e-ea5c-4cb9-9072-d28b8aba4c36/12140_Skull_v3_L2.mtl"}

 

That's three files and a directory to hold them. I'm not sure what the .mtl file does, and this post is mostly about the .obj file. The .jpg is really interesting, and it gets used later, and looks like this:

jpgblob=za<<read(members[2],format("blob"));
img=open(jpgblob,"jpg");
img<<scale(.25);
newwindow("jpg",img);

Texture map for skull, 1/4 sizeTexture map for skull, 1/4 size

The OBJ file contains the rest of the data: 3D points and their 2D texture map coordinates. There's an explanation of the file format here. The file contains v-sections of 3D coordinates, vt-sections of texture map coordinates, vn-sections of normal vectors, f-sections of faces (3 or 4 side polygons), and a few other items.

# 3ds Max Wavefront OBJ Exporter v0.97b - (c)2007 guruware
# File Created: 21.12.2011 14:29:19

mtllib 12140_Skull_v3_L2.mtl

#
# object 12140_Skull_v3_jaw
#

v  -1.6458 -14.7959 1.0976
v  -1.2548 -14.9644 1.0679
...
v  0.8637 -15.1231 0.7422
v  0.4355 -15.2145 0.7175
# 6114 vertices

vn -0.4113 -0.8998 0.1457
vn -0.3415 -0.9275 0.1520
...
vn 0.2578 -0.9662 0.0017
vn 0.1450 -0.9890 -0.0288
# 6114 vertex normals

vt 0.2309 0.7002 0.0000
vt 0.2363 0.7000 0.0000
...
vt 0.2632 0.6958 0.0000
vt 0.2578 0.6955 0.0000
# 6434 texture coords

g 12140_Skull_v3_jaw
usemtl 12140_Skull_v3
s 2
f 1/1/1 2/2/2 3/3/3 4/4/4 
f 2/2/2 5/5/5 6/6/6 3/3/3 
f 7/7/7 8/8/8 3/3/3 6/6/6 
f 8/8/8 9/9/9 4/4/4 3/3/3 
f 5/5/5 10/10/10 11/11/11 6/6/6 
f 10/10/10 12/12/12 13/13/13 11/11/11 
f 14/14/14 15/15/15 11/11/11 13/13/13 
...
f 6109/6429/6109 6108/6428/6108 6113/6433/6113 6114/6434/6114 
f 3086/3247/3086 3097/3258/3097 6114/6434/6114 6113/6433/6113 
f 3097/3258/3097 12/12/12 3072/3233/3072 6114/6434/6114 
# 6112 polygons

#
# object 12140_Skull_v3_teeth
#
...

 

Above, a very abridged copy of the file showing #comments, blank lines, a triangular face with three points and a bunch of quad faces, and some g and s records that I'll be ignoring.

 

JSL can read those efficiently with a pattern match. First, get the OBJ into a string:

obj=za<<read(members[3]); // 6MB polygon data

String( "# 3ds Max ... 6171025 total characters ...lygons

" ) assigned.

 

Then parse it. Set up some lists to hold the data values as they are parsed, and make some helper patterns:

vertex = {};
normal = {};
texture = {};
quad = {};
triangle = {};
seg={};
number=patspan("0123456789-.");
threenums = number>>n1+" "+number>>n2+" "+number>>n3+patspan(" \!n\!r");
integer = patspan("0123456789");

threetriples = 
integer>>n1+"/"+integer>>n2+"/"+integer>>n3+patspan(" ")+
integer>>n4+"/"+integer>>n5+"/"+integer>>n6+patspan(" ")+
integer>>n7+"/"+integer>>n8+"/"+integer>>n9;

fourthtriple = 
integer>>n10+"/"+integer>>n11+"/"+integer>>n12;

some of the faces are triangles, some are quads. The threetriples and fourthtriple are used below to efficiently parse and store the face in the proper list: quad or triangle.

objpat = 
("f"+patspan(" ")+
	// in this file, the "f" records all look like one of these:
	// f 10714/11419/10714 10713/11418/10713 10658/11363/10658 <<< trailing space
	// or
	// f 39694/42282/39694 39692/42679/39692 40060/42677/40060 39722/42283/39722 <<< trailing space
	threetriples+
	(
		(patspan(" ")+fourthtriple+patspan(" \!n\!r")+ //
			pattest(insertinto(quad,evallist({evallist({num(n1),num(n2),num(n3),num(n4),num(n5),num(n6),num(n7),num(n8),num(n9),num(n10),num(n11),num(n12)})}));1))
		| //
		(patspan(" \!n\!r")+ //
			pattest(insertinto(triangle,evallist({evallist({num(n1),num(n2),num(n3),num(n4),num(n5),num(n6),num(n7),num(n8),num(n9)})}));1))
	)
) | // in this file, the v, vt, vn records have x,y,z values
("v"+patspan(" ")+threenums+pattest(insertinto(vertex,evallist({evallist({num(n1),num(n2),num(n3)})}));1))|
("vt"+patspan(" ")+threenums+pattest(insertinto(texture,evallist({evallist({num(n1),num(n2),num(n3)})}));1))|
("vn"+patspan(" ")+threenums+pattest(insertinto(normal,evallist({evallist({num(n1),num(n2),num(n3)})}));1))|
("g"+patspan(" ")+
	patbreak("\!n\!r")>>SegName+
	pattest(insertinto(seg,// 2 is quads, 3 is triangles, 1 is name (mostly unused)
		evallist({evallist({segname,nitems(quad),nitems(triangle)})}));
	1)+
	patspan("\!n\!r")
) |
("#"+patbreak("\!n\!r")+patspan("\!n\!r")) |
(patbreak("\!n\!r")>>log("unused data")+patspan("\!n\!r"));

Objpat, defined above, holds a pattern; nothing has been parsed yet. Objpat tells the pattern matcher (below) how to match a single line in the OBJ file. Here's the call to the pattern matcher, with a little bit more pattern matching JSL to make the matcher loop through all the records in the file. 

It says: using the text previously loaded into obj, start at left position 0 and repeatedly use the objpat pattern until right position 0. After each successful objpat, fence off the work that was already done so JMP doesn't have to do extra record keeping.

rc = patmatch(obj,
	patpos(0)+
	patrepeat(objpat+patfence())+
	patrpos(0)
);
show(rc);

99(unused data) mtllib 12140_Skull_v3_L2.mtl

487834(unused data) usemtl 12140_Skull_v3

487857(unused data) s 2

1305132(unused data) usemtl 12140_Skull_v3

1305155(unused data) s 1

2038681(unused data) usemtl 12140_Skull_v3

2038704(unused data) s 1

4326305(unused data) usemtl 12140_Skull_v3

4326328(unused data) s 2

rc = 1;

 

rc==1 is a good thing, the pattern matched from beginning to end. The log() function in objpat is creating the lines about the unused data; it looks like those data lines won't be useful. And in the process, the matcher executed this JSL for each vn record (and similar for the other records):

pattest(insertinto(normal,evallist({evallist({num(n1),num(n2),num(n3)})}));1)

which filled the JSL list normal with all the vertex normals. (Normal vectors tell OpenGL what direction light will bounce off a surface.) The ;1 at the end tells pattest() that it should succeed so the matcher continues moving forward.

The vertex list is a list of 3-item lists; the items are x, y, z coordinates:

{{-1.6458, -14.7959, 1.0976}, 
{-1.2548, -14.9644, 1.0679}, 
{-1.2202, -14.8974, 1.4306}, 
{-1.6069, -14.7373, 1.4505}, 
{-0.8477, -15.1013, 1.036}, 
{-0.8223, -15.0288, 1.403}, 
{-0.7966, -14.9354, 1.8162}, 
{-1.1896, -14.811, 1.8424}, 
{-1.5761, -14.6593, 1.8511}, 
{-0.4272, -15.1961, 1.01}, 
{-0.4138, -15.1209, 1.3778}, 
{-0.0001, -15.2327, 0.9984}, 
{-0.0001, -15.1568, 1.3658}, 
{0, -15.0543, 1.7793}, 
{-0.3993, -15.0212, 1.7914},
...

The Faces list is separated into triangles and quads. The triangle list is also a list of lists:

{{6148, 6468, 6148, 6150, 6470, 6150, 6151, 6471, 6151}, 
{6149, 6469, 6149, 6148, 6468, 6148, 6151, 6471, 6151}, 
{6174, 6494, 6174, 6175, 6495, 6175, 6151, 6471, 6151}, 
{6150, 6470, 6150, 6174, 6494, 6174, 6151, 6471, 6151}, 
{6198, 6518, 6198, 6199, 6519, 6199, 6151, 6471, 6151}, 
{6175, 6495, 6175, 6198, 6518, 6198, 6151, 6471, 6151}, 

those three triples are indexes into the vertex, texture, and normal lists. The texture list uses 0..1 coordinates to pick a color from the texture map, way above. The JSL list seg has four items, each a list containing a segment name (skull, jaw, upper teeth, lower teeth) and starting locations in the triangle and quad lists.

This JSL loads the data into JSL SceneDisplayLists for each of the seg items:

// load model into OpenGL
// seg describes 4 segments (jaw, top teeth, bot teeth, skull)
// for Halloween I want two segments, skull and jaw, with the
// teeth welded on already.
// so, load the segs, then recombine them...

displaylists = {};
for(iDisplayList=1,iDisplayList<=nitems(seg),iDisplayList +=1,

	partial = Scene Display List();
	partial << BEGIN( QUADS );
	// the seg notes its startinq quad/triangle, use the next seg
	// to find this segs end. or the rest, for the last seg.
	// 2 is quads, 3 is triangles
	lastquad = if(iDisplayList < nitems(seg),seg[iDisplayList+1][2],nitems(quad));
	for(i=seg[iDisplayList][2]+1,i<=lastquad,i+=1,
		for(q=0,q<12,q+=3,
			partial << Normal( normal[quad[i][q+3]][1],  normal[quad[i][q+3]][2],  normal[quad[i][q+3]][3] );
			tx=texture[quad[i][q+2]][1];
			ty=texture[quad[i][q+2]][2];
			c=jpg[(1-ty)*(nrows(jpg)-1)+1,(tx)*(ncols(jpg)-1)+1];
			partial << Color( c );
			partial << Vertex( vertex[quad[i][q+1]][1],  vertex[quad[i][q+1]][2],  vertex[quad[i][q+1]][3] );
		);
	);
	partial << END;
	partial << BEGIN( TRIANGLES );
	lasttriangle = if(iDisplayList < nitems(seg),seg[iDisplayList+1][3],nitems(triangle));
	for(i=seg[iDisplayList][3]+1,i<=lasttriangle,i+=1,
		for(t=0,t<9,t+=3,
			partial << Normal( normal[triangle[i][t+3]][1],  normal[triangle[i][t+3]][2],  normal[triangle[i][t+3]][3] );
			tx=texture[quad[i][t+2]][1];
			ty=texture[quad[i][t+2]][2];
			c=jpg[(1-ty)*(nrows(jpg)-1)+1,(tx)*(ncols(jpg)-1)+1];
			partial << Color( c );
			partial << Vertex( vertex[triangle[i][t+1]][1],  vertex[triangle[i][t+1]][2],  vertex[triangle[i][t+1]][3] );
		);
	);
	partial << END;

	displaylists[iDisplayList] = partial;
);

That's the same logic, twice, once to make triangles, and once to make quads.

That JSL made four display lists from the four parts; the attached file will combine and animate them and add some Halloween-style eyeballs. Time for a picture...

 

Halloween early evening, model in windowHalloween early evening, model in window

Does it look like the eyes are following the photographer? What's with the gap at the bottom of the window?

 

Arduino Nano and PIR sensors

To make the eyes track people, some sort of sensor is needed. PIR sensors have a single-bit interface (something moved!) and some limitations on response time. Given the low turnout on Halloween, they were the right choice. A Raspberry PI camera could provide better performance. The gap below the window allows five PIR sensors with collimators to watch regions where people might walk. Here's the setup:

 

They do like like eyes. Those collimator tubes are re-purposed.They do like like eyes. Those collimator tubes are re-purposed.

And here's what the assembley looks like, taped, hot-glued, and jumper-wired to the Arduino Nano. The Nano is small.

 

Arduino Nano on left under zip-ties. PIR sensors on right hot-glued to collimators. There are two orange adjustments on each PIR.Arduino Nano on left under zip-ties. PIR sensors on right hot-glued to collimators. There are two orange adjustments on each PIR.

The Nano clone came with header pins that can be soldered on, or wires can be soldered to the Nano. Cutting jumper wires in half and soldering one end makes it a little less likely to fall apart and a little more likely to be reused in another project.

 

Nano has wires soldered on, orange is +5, green is ground, purple/gray/blue are data linesNano has wires soldered on, orange is +5, green is ground, purple/gray/blue are data lines

Here's the code that runs on the Nano; use the Arduino IDE to compile and download via the USB connection. The IDE makes it easy to discover the COM port number that is assigned to the USB port. You'll need that in the JSL too. The attachment has an extra .txt extension, rename it to just have the .ino extension.

 

Arduino IDE showing code to read sensors and send to JMPArduino IDE showing code to read sensors and send to JMP

The setup routine runs once; the loop routine is called again and again, forever. Delay(100) waits 1/10 second before the loop runs again. 2,3,4,5,6 are the pins connected to the sensors. Anything printed to the serial port can be read by JMP using the DataFeed. Here's another snippet of the JSL for that, using the Word() function to get the five sensor values from the data line:

sensorBits = [0, 0, 0, 0, 0];
feed = Open Datafeed(
	Connect( Port( "com4" ), Baud rate( 115200 ), Parity( none ), DataBits( 8 ) ),
	Set Script(
		Local( {i, txt, temp = sensorBits},
			txt = feed << getLines;
			for(iline=1,iline<=nitems(txt),iline+=1,
				For( i = 1, i <= N Rows( sensorBits ), i += 1,
					temp[i] += Num( Word( i, txt[iline], "," ) )
				);
				If( Any( Is Missing( temp ) ),
					Show( temp,chartoblob(txt[iline]) ),
					sensorBits = temp * .9;
					try(tb<<settext( char(Loc Max( sensorBits ))||" "|| Char( Round( sensorBits, 2 ) ) ););
				);
			);
		)
	)
);
Window( "Datafeed" ) << onclose(
	feed << disconnect;
	try(mon<<closewindow);
	1;
);

mon<<onclose(try(feed<<close;1););

The datafeed needs to be closed correctly or it will find the port still busy the next time; that's what the OnClose logic is doing. Everything else is just keeping the sensorBits matrix updated by incrementing if a sensor bit is on and fading if it is off. The sensors stay on for at least three seconds and the counts get up to about 10 and fairly quickly fade back down to nearly zero.

 

That's about it; there is a loop in the JSL (attachment), near the end, that rotates the eyes and the model in response to the sensors...and detects when the skull is bored and runs some animations to attract trick-or-treaters. The JSL will probably run with the datafeed even without anything connected; it might be unhappy with the com port, and might be really unhappy if run a second time. Should be easy to comment out the com port. 

The tracking eyeballs do work, with several seconds of lag. It helps to know where to stand. The animations made it hard for the audience to know they were being watched.

Also, JMP doesn't really support texture mapping currently (JMP 15), so this is a bit slow. Using an older laptop I was still getting 15 FPS, good enough for Halloween. 

 

8pm, about to rain. Candy Corn half gone, glow sticks popular, vampire teeth and bubbles too!8pm, about to rain. Candy Corn half gone, glow sticks popular, vampire teeth and bubbles too!

And that projector is bright! Here's the special projector stand, projecting onto a translucent window shade:

 

Not recommended if you have a cat.Not recommended if you have a cat.

And, Previous Halloween trilogy post. 

 

short loop of animation possibilityshort loop of animation possibility

Last Modified: Nov 2, 2019 12:03 AM