Notifications
Clear all

[Closed] Export explicit normals

So I’ve found something interesting with max, it seems that by using the GetNormal argument does not grab explicit normals, infact it actually grabs specified normals instead. I need to export explicit normals otherwise the normals in the game im exporting the 3d to does not look correct. Is there a way to do this?

For a bit more information on the subject here is another thread where I made the discovery. http://forums.cgsociety.org/showthread.php?f=6&t=1164557

18 Replies

what are you using for the export ? sdk/mxs/python/dotnet ?

what getnormal ? the mesh vertex Normal ? you can access Explicit Normals from an Edit Normals modifier.

2 Replies
(@troopermanaic)
Joined: 11 months ago

Posts: 0

Im just using the built-in maxscript language.

Yes about the normals by the way I am getting vertex normals but heres the thing when I use the Normals Modifier and specify the vertex normals to be explicit it still “in the listener” gets specified normals if I use the GetNormal argument so its not really getting the normals seen in the view port even if I collapse all stacks.

clearlistener()
for obj in geometry do(
	for v =1 to obj.numverts do(
		NORMALS=getnormal obj v
	)
)
(@denist)
Joined: 11 months ago

Posts: 0

without sdk help the only solution is to use Edit_Normals modifier as suggested above.

Yes about the normals by the way I am getting vertex normals but heres the thing when I use the Normals Modifier and specify the vertex normals to be explicit it still “in the listener” gets specified normals if I use the GetNormal argument so its not really getting the normals seen in the view port even if I collapse all stacks.

the getnormal function only ever returns a normal that is computed based on the faces that use that vert and the smoothing groups assigned to those faces. The explicit normals that you create via the edit normals modifier are held else where, to access them you have to go via the exposed mxs functionality of the edit normals modifier.

here’s denis’s example of how to copy “explicit” normals from an edit normals modifier to an mapping channel, subdividing the mesh and then copying them back. Though not exactly what you need it’s a good example of accessing the data you want.

It is doing exactly what it describes in the help here:

getNormal <mesh> <vert_index_integer>

Returns the normal at the indexed vertex’s position as a point3. The normal is based on the faces that use the vertex and the smoothing groups assigned to those faces.
Maxscript in edit/editable mesh/poly doesn’t expose a method to get vertex/explicit normals only face/specified normals.

-Eric

After doing some testing I have noticed something unusual, I wanted to compare what normals max gets in memory vs what normals it exports in an .obj file here is what I found useing the following settings. “un-optimized everything, and unchecked smoothing groups”. Here is what the object looks like in another program

so the .obj exporter does export explicit normals because the poly count is the same as in max and all smooth groups from the object are stripped this is what I need to include into my exporter for my game.

Digging a bit more on a scientific level I notice in the .obj file itself has completely different values for normals then what I can get from using the GetNormal argument, I am wondering does anybody know where I can find the SDK to the .obj exporter for max?

The new OBJ import/export is a 3rd party acquisition and not sure if the source code is available in the SDK samples.

-Eric

if you intend to venture into the sdk for your exporter the IGame wrapper will handle explicit normals right off the bat (it will also compute tangents and binormals for you too). The sample IGame exporter in the sdk is a good place to start.

1 Reply
(@troopermanaic)
Joined: 11 months ago

Posts: 0

Thanks allot, seems like a great place to learn things I didn’t know before. I’m starting to realize that there could be more normals then verts now which is why my exporter isn’t functioning properly. I guess imma have to learn how to properly optimize models beforehand. I think utilizing that information I can get better at making exporters for the games I like to play.

I always thought that I have made good models and what I make can be considered art but after playing around with game engines I now realize that there is more to model optimization then just combining verts and ridding faces.

yep a proper game engine exporter is a non trivial exercise. converting max’s face based meshes correctly into an optimized vertex & indices streams is quite tricky. I use the following approach…

for each material id in the mesh....

1. start with the worst case scenario, that is for every tri face there is 3 unique verts (where a vert has position, normal, vertex colour, tangent, binormal, uv1, uv2, etc and any other vertex stream data your engine handles).  Doing this we can keep all out uv seams, smoothing group edges etc.

2. From this worst case list create a list of optimized "unique" verts (to do this you need to implement some rather ugly comparison code, verts/normals/uv/colours don't do greater than/less than/equals very well or use some hashing technique, more elegant and faster though does risk unforeseen hash collisions)

3. Rebuild face indices by finding where each vert in the worst case list is
found in the best case list.

i was bored so i wrote a small demo of the principle


       --example of a  simple game vertex buffer exporter
       
       -- can't use built in hash as it hashes mirror points as the same, and we can't cycle the hash either has as we need the same
       -- hash for equivalent verts & normals so we use xor and large primes
       
       fn hashPoint3 n = 
       (
       	hash = (n.x  * 73856093) as integer;
       	hash = bit.xor hash ((n.y  * 19349663) as integer );
       	bit.xor hash  ((n.z  * 83492791) as integer );
       )
       
       -- our game vertex format
       
       struct vertex
       (
       	pos,
       	normal,
       	col,
       	uv,
       	hash,
       
       	fn hashit =	
       	(
       		hash = (hashPoint3 pos) * 93944371;
       		hash = bit.xor hash ((hashPoint3 normal ) * 36311839);
       		hash = bit.xor hash ((hashPoint3 (col as point3)) * 82895123);
       		hash = bit.xor hash ((hashPoint3 uv) * 19393541);
       	),
       	fn printf = (format "% % % % %
" pos normal col uv hash);
       )	
       
       --**************************************************************************************
       -- sort and search compares, the actual search order is meaningless just
     -- as long as it's repeatable and consistent !
       
       fn compareVertexfn i j values: =
       (
       	v1 = values[i].hash; 
       	v2 = values[j].hash;
       	if v1 < v2 then 1 else if v1 > v2 then -1 else 0;
       )	
       
       fn compareVertexBinfn i j raw: opt: =
       (
       	v1 = raw[i].hash; 
       	v2 = opt[j].hash;
       	if v1 < v2 then 1 else if v1 > v2 then -1 else 0;
       )	
       
       --***********************************************************************************
       -- collects all the unique material IDs within a mesh
       
       fn GetMeshMatIDs mObj =
       (
       	matid = #();
       	for f in mObj.faces as bitarray	do appendifunique matid (getfaceMatID mObj f);
       	matid;
       )	
       
       --***********************************************************************************
       -- collects all the faces with the same material ID
       
       fn GetFacesWithMatID mObj matid =
       (
       	faces = #{}
       	faces.count = mObj.numfaces;
       	for f in mObj.faces as bitarray do faces[f] = ((getfaceMatID mObj f) == matid);
       	faces;
       )	
       
       --**************************************************************************************
       -- generate the worst case scenerio
       
       fn CollectRawVerts msh faces nMod tm =
       (
       	verts = #();
       	verts.count = faces.numberSet * 3;
       	vi = 1;
       	for f in faces do
       	(
       		geo_verts = getface msh f;
       		tex_verts = getTVFace msh f;
       		cpv_verts = getVCFace msh f;
       		for v = 1 to 3 do
       		(
       			gvert = (getvert msh geo_verts[v]) * tm;
       			tvert = getTVert msh tex_verts[v];
       			cvert = getVertColor msh cpv_verts[v];
       			normal = nMod.GetNormal (nMod.GetNormalID f v);
       
       -- create the vertex and generate it's hash
       			
       			verts[vi] = vertex gvert normal cvert tvert;
       			verts[vi].hashit();
       			vi += 1;
       		)	
       	)	
       	verts;
       )	
       
       --**************************************************************************************
       -- create the optimized version of the raw verts
       
       fn OptimizeVerts raw =
       (
       	numverts = raw.count;
       	copylist = #();
       	copylist.count = numverts;
       	
       -- create the index array	
       	
       	rawi = #();
       	rawi.count = numverts;
       	for i = 1 to numverts do rawi[i] = i;
       
   -- sort int indexes based on the raw data	
   
       	qsort rawi compareVertexfn values:raw;
       	
   -- collect the indices of all the "unique" verts	
   
       	copycount = 0;
       	for i = 1 to numverts do 
       	(
       		copylist[i] = -1;	
       		if i == bsearch i rawi compareVertexfn values:raw then
       		(	
       			copycount += 1;
       			copylist[copycount] = i;
       		)	
       	)	
   
   -- extract them from the raw verts	
   
       	optimized = #();
       	optimized.count = copycount;
       	copycount = 1;
       	for i = 1 to numverts do 
       	(
       		if i == copylist[copycount] then
       		(	
       			optimized[copycount] = raw[i];
       			copycount += 1;
       		)
       	)
       	optimized;	
       )	
       
       --***********************************************************************************
       -- reconstruct the face indexing, the raw verts are in the correct order so we find where
   -- each raw vert is in the optimised array and thats our indices.
       
       fn CreateFaces raw optimized =
       (
       	opti = #();
       	opti.count = optimized.count;
       	for i = 1 to optimized.count do opti[i] = i;
       		
       	qsort opti compareVertexfn values:optimized;
       	for i = 1 to raw.count collect (bsearch i opti compareVertexBinfn raw:raw opt:optimized;)
       )	
       
       --***********************************************************************************
       -- test the routine by building a new mesh based on the data
       
       fn BuildMesh verts faces =
       (
       	nverts = verts.count;
       	nfaces = faces.count/3;
       	msh = mesh numverts:nverts  numfaces:nfaces;
       
       	for v = 1 to nverts do setvert msh v verts[v].pos;
       	fi = 1;
       	for f = 1 to faces.count by 3 do 
       	(	
       		setface msh fi faces[f] faces[f+1] faces[f+2];
       		setEdgeVis msh fi 1 true;
       		setEdgeVis msh fi 2 true;
       		setEdgeVis msh fi 3 true;
       		fi += 1;
       	)	
       	update msh;
       	msh;
       )	
       
       --**************************************************************************************
       
       fn ExportMesh mObj = if canConvertTo mObj Editable_Mesh then
       (
       -- grab the mesh	
       	
       	msh = snapshot mObj;
       	
       -- compute the inverse tm so we can get positions in local space	
       	
       	localTM = (inverse (msh.transform));
       		
       -- check for color per vertex if not there create a default		
       		
       	if getNumCPVVerts  msh == 0 then defaultVCFaces msh;
       		
       -- get the number of mat id's	
       	
       	matids = GetMeshMatIDs msh;
       	
       -- get normals	
       	
       	select msh;
       	setCommandPanelTaskMode mode:#modify
       	modpanel.addmodtoselection (norm = edit_normals displayLength:0);	
       	
       	for m in matids do
       	(
       		faces = GetFacesWithMatID msh m;
       		raw = CollectRawVerts msh faces norm localTM;
       		opt = OptimizeVerts raw;
       		indices = CreateFaces raw opt;
       		
       		-- export to file from here
       		
       -- the proof it works		
       		BuildMesh opt indices;
		select msh;
       	)	
       	delete msh;
       )	
       	
       delete objects
       sph = sphere segs:32  mapcoords:on pos:[75,0,0]
       clearlistener()
       ExportMesh sph;
   the new sphere mesh as you can see is broken along the UV seam (I don't set the normals when creating the proof mesh so max creates his own.) If you create you own mesh which various "hard" seams e.g vertex colour, smooth groups and UV's  and run it through the ExportMesh function you can see how the faces are split into elements. Also it's worth noting that the indices are in a 1 based max array format, but this is easily correct if you need them as zero based.
Page 1 / 2