MaxScript porting help (setTVert and setTVFace)

I have been porting a MaxScript custom model import script to JavaScript using three.js. I got all of the logic ported. The last part is building the mesh from the retrieved data. This was easy for mesh vertices and faces. However, it gets extremely complicated when it comes to the UV data. I get the UV data in an array of three point value arrays as like [-0.600184977054596, 1.9818799495697021, 0] (long numbers but comparing output from actual script, the values are correct. Also, third point is always 0). Every example I have seen for the faceVertexUvs has been like:

geometry.faceVertexUvs[0].push(
  // front
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  // right
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  // back
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  // left
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  // top
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
  // bottom
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, 1) ],
);

Source

However, the original script does it like:

for u = 1 to (mesh_uv_array.count) do setTVert cmdlmesh u mesh_uv_array[u]
for u = 1 to (mesh_face_array.count) do setTVFace cmdlmesh u mesh_face_array[u]
for u = 1 to (mesh_face_array.count) do setFaceMatID cmdlmesh u mesh_mat_array[u]

I’m not sure how to port this exactly. Going by the definition of setTVert and setTVFace, 3DS Max appears to automatically calculate UVs based on an array of vertexes and faces? I’m not exactly sure but the logic doesn’t match up with how three.js does it. I’d appreciate some help here.

PS:
Uing just the faces and vertices, I do get the mesh to successfully appear. So I did get that part down.

Max is basically treating this as two unique meshes that happen to align because they have the same amount of triangles. If you look at the original output from this exporter, you’ll see a very complex face definition (if i remember correctly) or possibly two sets of faces/vertices.

Your job is to combine these into a single mesh. If using indexed geometry, you will most likely need to duplicate (position) vertices, to account for the duplicated normals and uv islands. Try exporting a cube, with some smoothing groups.

If that is how it works, how does it get the UVs in the first place? Even if I did that, I still wouldn’t have the UVs… just a second mesh merged into one…
I make the initial mesh (which displays with a blank MeshBasicMaterial) this way:

	// -- Now we finally create the mesh!
	let cmdlgeometry = new THREE.Geometry();
	
	// Mesh Vertices
	mesh_vertex_array.shift(); // Remove first element for one-based index -> zero-based index difference
	cmdlgeometry.vertices =  mesh_vertex_array.map(vp => new THREE.Vector3(vp[1], vp[2], vp[3])); // one-based index is still used for each vertex array
	
	// Mash Faces
	mesh_face_array.shift(); // Remove first element for one-based index -> zero-based index difference
	cmdlgeometry.faces = mesh_face_array.map(fp=> new THREE.Face3(fp[1]-1, fp[2]-1, fp[3]-1)); // one-based index is still used for each face array and value is subtracted for one-based index -> zero-based index difference

After is where the UV assignment happens which again, I am still having trouble figuring out a 1:1 for this. Again, you say by these methods it’s making a second mesh and seeing how to intercept but how does this have to do with anything UV related? You say “make another mesh and merge dealing with duplicates” but that gets nowhere close to getting the UVs or using the UVs I have on hand…

You noticed yourself that the uv is a vec3, it has an x, a y, and a z.

This is no different than having a vertex, that is somewhere in 3d space. Are you familiar with the channel info tool in 3ds max? You can open it and try copying the UV channel into the position channel and see what happens.

Also notice there that each channel may have a different number of elements. Try assigning a single smooth face to the entire cube, and look at the number of vertices. Then try to autosmooth to give each face a different group, look at the normal channel.

Same for uvs, try fiddling with UV Unwrap tool and breaking up some triangles, the vertex count should increase.

Now, while each channel has the same number of triangles (indices) it’s a completely different element array (position, uv, normal, tangent, vertex color, foo) .

If you make two copies of your cube, copy the normal channel into position in one, uv in the other, you will see 3 different meshes / objects in your viewport, each an editable mesh.

This is what i meant by “its just a mesh” - it is, since it has some element data, and some indices (triangles).

If you simply unpack each of these element + indices pair, into a single array that is a “triangle soup” they will simply align.

const vertices = []
const uvs = []
const normals = [] 

for ( let i = 0 ; i < triangleCount ; i ++ ) {
 for ( let j = 0 ; j < 3 ; j ++ ) {
  vertices.push( ...vData[ vertexFaces[ i*3 + j ] ])
  uvs.push( ...uvData[ uvFaces[ i*3 + j ] ])
  normals.push( ...nData[ normalFaces[ i*3 + j ] ])
 }
}

Ie.

  for each triangle
    get abc of
      vertexFace
      uvFace
      normalFace
      arbitraryFace
   get data with abc in
     vertexData
     uvData. 
     ...
   put in one triangle soup
     vertex.position
     vertex.uv
     vertex.normal

Also,

mesh_vertex_array.shift();

Is most likely wrong, it should be:

mesh_FACE_array = mesh_FACE_array.map(v=>v-1)

I still don’t quite understand, even with the pseudo code…
You keep saying “element + indices” but I don’t have UV faces and UV indices. All I have is UV vertices.
The only data I have to work with is mesh_vertex_array which is an array of vertex data in groups of arrays of size 3 ([[0.316222, -0.0293346, -0.017658], ...]), mesh_face_array which is an array of vertex indices in groups of arrays of size 3 ([[1, 2, 3], ...]), mesh_uv_array which is an array of more vertex data in groups of arrays of size 3 where third index is always zero ([[-0.600185, 1.98188, 0], ...]), and mesh_mat_array which is an array of material indices for each face ([5, 5, 5, 5, 5, 5, 5, ...]). The original script does not retrieve actual normal values. It only gets the normals for each submesh to weld vertices but discards them later on so I have no normals to work with. Three can auto calculate normals so I’m not worrying about that.

I guess what I am asking is how am I supposed to make an entire other mesh with ONLY UV vertexes and also, in the end I’d only have another mesh and no UV data so where does Max pull the UV data from after making the second mesh? I am severely confused…

PS: shift is also a shortcut to remove the first index of an array and is faster based on jsperf results.

I have looked into AutoDesk’s official explanation on how texture coordinates work in 3DS Max and it appears each UVW is for each face index of each vertex for which the coordinate is the coordinate position of a pixel on an image. However, Three does it as 0 to 1 texture space, not by coordinates so how do I get the required data from just the coordinates?

I have to admit that i’m confused with what you are trying to do.

setTVert - this works with a vertex
setTFace - this works with a face

Did someone already do this mapping, hence you only have uvs? Not sure if you have to even do any work in this case.

const pAttr = new BufferAttribute( new Float32Array( myVertices), 3 )
const uvAttr = new BufferAttribute( new Float32Array( myUvs), 2 )

I don’t think that this is true. Like i said you can use the channel info util to see whats going on. If you open the uvw editor, youll see the values. They can be 0-1, 0-3411284, -73.4546 - 1.345, or any range a computer can deal with more or less.

I think you might be confused about this:

Say you have three vertices:

const vertices = [a,b,c]

And you have one triangle, you got from max:

const triangle = [1,2,3]

This won’t work because your triangle witll be:

[b,c, ???]

You need to shift this so it’s:

const triangle = [0,1,2] // [a,b,c]

shift() does:

const fromMax = [1,2,3]
fromMax.shift() // [2,3] <- not a triangle any more, only two indices, and 3 doesnt exist.

Upon rereading your question, it may be How do i set the uv attribute on the Geometry class. It doesnt seem to have anything to do with how max stores it’s mesh data, how some exported used to export this in the past etc. You already have a formatted array, you just need to put it in the right place.

Ergo:

geometry.faceVertexUvs[0].push(
  // front
  [ new THREE.Vector2(0, 0), new THREE.Vector2(1, 1), new THREE.Vector2(0, 1) ],
  ....
)

Should be:

geometry.faceVertexUvs[0].push(
  // front
  [ 
    new THREE.Vector2(YOUR_UV[0], YOUR_UV[1]), 
    new THREE.Vector2(YOUR_UV[3], YOUR_UV[4]), 
    new THREE.Vector2(YOUR_UV[6], YOUR_UV[7]), 
  ...

So UV data can be as decimal data? If I do cmdlgeometry.faceVertexUvs[0] = mesh_uv_array.map(uvp=> new THREE.Vector2(uvp[1], uvp[2])); for which I get a lot of THREE.BufferAttribute.copyVector2sArray(): vector is undefined # warnings. If I loop through the array and assign UVs in groups of three Vector2s, the UV is severely incorrect with textures appearing warped. This is why I believe the UV data to be vertex data and not UV data, especislly with this setTVert and setTVFace stuff. I was also going by what I read in the AutoDesk help section Finding the corresponding vertices in Understanding Texture Coordinates and Vertex Colors. Also, the first index is always undefined (for ease on porting, I did this to replicate one-based index arrays) so I shift to remove the first index. For each array in the array, the first index is still undefined so I do [1], [2], [3] instead of [0], [1], [2]. Also, I don’t know the “front”, “back”, “top”, etc. for the UV data. As I’ve stated before, all I have is an array of UV data in groups of arrays of size 3.

It’s more like, I don’t know how to take the UV data I have and store it to faceVertexUvs correctly.

I’m not super familiar with the Geometry class, i thought it was removed. But yes, UV data can be decimal data (of any precision).

Is there any chance you could tackle your problem with BufferGeometry?

Perhaps doing another snippet:

const myPositions = /*tell us the length, the format*/
const myFaces = /*tell us the length, the format*/
const myUvs = /*tell us the length, the format*/
/*... any other DATA ...*/

const geom = new BufferGeometry()

/* what do i do here */

If I loop through the array and assign UVs in groups of three Vector2s, the UV is severely incorrect with textures appearing warped.

This might be due to new THREE.Vector2(uvp[1], uvp[2]))

Try new THREE.Vector2(uvp[0], uvp[1]))

I don’t understand how you’re getting an undefined first index. Again, if you are using max script, i think this is not as trivial as you might imagine it, and the whole “two meshes” concept comes to play. If you just got some data, post it here so it can be understood.

If you don’t know what to ask, i suggest making a simple quad (two triangles) with BufferGeometry to get a better grasp on what’s going on.

First start with a triangle and triangle soup, ie no indices, just positionis, then add vertices, then add another triangle, then try to reduce the number of positions and uvs, and use index attribute, and then try to split the two UV triangles, while keeping the positions (it actually can’t be done, so you will revert to the triangle soup). I think this will clarify a lot.

Also, if you own the entire pipeline, make a plane in 3ds and export that, post the result here.

I purposefully do a index[0] = undefined to replicate one-based array index that MaxScript uses. Then I remove this when actually using the data. Also, as before I stated the data I have to work with. The problem is how do I organize this data in the way Three wants it? I keep seeing the same examples of arrays of 3 Vector2s with texture coordinates of 0-1 for the same cube example again and again but it does not help me with how I should organize the data in the format I have it. I hear explanations like “coordinates in counter clockwise” and examples of arays of size 3 with 3 vector2s with arbitrary texture coordinates in specific order for each vertex2. The issue is I don’t have any arbitrary data so all I can do is loop and work with arrays of data.

The model data is attached to this post.model_data.js (260.5 KB)

This seems to work for me:

const geom = new THREE.BufferGeometry()

const positions = mesh_vertex_array.reduce((acc,curr)=>{
  acc.push(...curr) 
  return acc
},[])

const uvs = mesh_uv_array.reduce((acc,curr)=>{
  acc.push(curr[0],curr[1])
  return acc
},[])

const faces = mesh_face_array.reduce((acc,curr)=>{
  acc.push(...curr)
  return acc
})

geom.setIndex( new THREE.BufferAttribute(
  new Uint16Array(faces),1
))
geom.addAttribute('position', new THREE.BufferAttribute(
  new Float32Array(positions),3
))

geom.addAttribute('uv', new THREE.BufferAttribute(
  new Float32Array(uvs),2
))
scene.add( new THREE.Mesh(geom))

Can’t fork without registering, but pasting your js and this code into https://codepen.io/agconti/pen/Zejwjw loads a mesh. Ie, i get something that looks like it has mapping sort of:

image

(it’s all negative and not normalized, but thats fine)

Is this what you were trying to do? How does max script fit into this, and the offset indices?

You use BufferGeometry but this has no different effect for the UVs (even with the times I did get UVs in the correct structure for normal Geometry, the UVs get the same warped incorrect look)

EDIT:
Actually, after exporting to OBJ and importing into Max and using the UVW Unwrap modifier to view the UVs, the UVs appear to be correct. I now suspect it’s because I am using multiple materials. mesh_mat_array contains a material index for each face. mesh_mat_array is the same length as mesh_face_array. The issue is, BufferGeometry is using Uint16Array as per your example (for which using on normal Geometry fails yet again with the UVs, for some reason, Vector2 does not work in the way you did it for Uint16Array) and therefore I cannot apply a materialIndex solution to this. I read up upon addGroup but it requires that I know where each group starts and the length of each group for each material index, for which I don’t know (and even if I sorted the data, the groups would not be aligned as faces are not grouped by material index).

I’m sorry, i don’t think i actually understand what’s your problem. Maybe asking the question again with only the relevant detail? Good luck!