How to define PyTorch3D plane geometry?
I'm new to PyTorch3D and I'm trying to define a (subdivided) plane.
What I've tried to do is mirror the structure I found in PyTorch3D's ico_sphere.py.
For reference these are the contents:
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.


import torch
from pytorch3d.ops.subdivide_meshes import SubdivideMeshes
from pytorch3d.structures.meshes import Meshes


# Vertex coordinates for a level 0 ico-sphere.
_ico_verts0 = [
    [-0.5257, 0.8507, 0.0000],
    [0.5257, 0.8507, 0.0000],
    [-0.5257, -0.8507, 0.0000],
    [0.5257, -0.8507, 0.0000],
    [0.0000, -0.5257, 0.8507],
    [0.0000, 0.5257, 0.8507],
    [0.0000, -0.5257, -0.8507],
    [0.0000, 0.5257, -0.8507],
    [0.8507, 0.0000, -0.5257],
    [0.8507, 0.0000, 0.5257],
    [-0.8507, 0.0000, -0.5257],
    [-0.8507, 0.0000, 0.5257],
]


# Faces for level 0 ico-sphere
_ico_faces0 = [
    [0, 11, 5],
    [0, 5, 1],
    [0, 1, 7],
    [0, 7, 10],
    [0, 10, 11],
    [1, 5, 9],
    [5, 11, 4],
    [11, 10, 2],
    [10, 7, 6],
    [7, 1, 8],
    [3, 9, 4],
    [3, 4, 2],
    [3, 2, 6],
    [3, 6, 8],
    [3, 8, 9],
    [4, 9, 5],
    [2, 4, 11],
    [6, 2, 10],
    [8, 6, 7],
    [9, 8, 1],
]


def ico_sphere(level: int = 0, device=None):
    &quot;&quot;&quot;
    Create verts and faces for a unit ico-sphere, with all faces oriented
    consistently.
    Args:
        level: integer specifying the number of iterations for subdivision
               of the mesh faces. Each additional level will result in four new
               faces per face.
        device: A torch.device object on which the outputs will be allocated.
    Returns:
        Meshes object with verts and faces.
    &quot;&quot;&quot;
    if device is None:
        device = torch.device(&quot;cpu&quot;)
    if level &lt; 0:
        raise ValueError(&quot;level must be &gt;= 0.&quot;)
    if level == 0:
        verts = torch.tensor(_ico_verts0, dtype=torch.float32, device=device)
        faces = torch.tensor(_ico_faces0, dtype=torch.int64, device=device)

    else:
        mesh = ico_sphere(level - 1, device)
        subdivide = SubdivideMeshes()
        mesh = subdivide(mesh)
        verts = mesh.verts_list()[0]
        verts /= verts.norm(p=2, dim=1, keepdim=True)
        faces = mesh.faces_list()[0]
    return Meshes(verts=[verts], faces=[faces])

This is my attempt of tweaking the above for a basic quad plane from 2 triangles:
import torch
from pytorch3d.ops.subdivide_meshes import SubdivideMeshes
from pytorch3d.structures.meshes import Meshes


# Vertex coordinates for a level 0 plane.
_plane_verts0 = [
    [-0.5000,-0.5000, 0.0000], # TL
    [+0.5000,-0.5000, 0.0000], # TR
    [+0.5000,+0.5000, 0.0000], # BR
    [-0.5000,+0.5000, 0.0000]  # BL
]

# Faces for level 0 plane
_plane_faces0 = [
    [2, 1, 0],
    [0, 3, 2]
]


def plane(level: int = 0, device=None):
    &quot;&quot;&quot;
    Create verts and faces for a unit plane, with all faces oriented
    consistently.

    Args:
        level: integer specifying the number of iterations for subdivision
               of the mesh faces. Each additional level will result in four new
               faces per face.
        device: A torch.device object on which the outputs will be allocated.

    Returns:
        Meshes object with verts and faces.
    &quot;&quot;&quot;
    if device is None:
        device = torch.device(&quot;cpu&quot;)
    if level &lt; 0:
        raise ValueError(&quot;level must be &gt;= 0.&quot;)
    if level == 0:
        verts = torch.tensor(_plane_verts0, dtype=torch.float32, device=device)
        faces = torch.tensor(_plane_faces0, dtype=torch.int64, device=device)

    else:
        mesh = plane(level - 1, device)
        subdivide = SubdivideMeshes()
        mesh = subdivide(mesh)
        verts = mesh.verts_list()[0]
        verts /= verts.norm(p=2, dim=1, keepdim=True)
        faces = mesh.faces_list()[0]
    return Meshes(verts=[verts], faces=[faces])

The plane with no subdivision (level=0) seems fine.
The issue I'm having is when I subdivide the plane, the result shows holes / flipped normals by the looks of it:

I've tried to change the face indices a bit (offsetting the starting point, using CW vs CCW winding order, etc.), but the result is the same, so I'm not sure if the issue with the plane geometry itself, the subdivision, or both.
(I haven't found a good way to visualise face normals. I've tried plotly's Mesh3D option, but as far as I can tell it's doing double sided rendering and I couldn't figure out how to set it's renderer to render triangles single sided only. Any tips on visually debugging are the geometry also welcome)
Update
I've tried something slightly different: copying the vertex and face from a (triangulated) plane drawn in Blender:
'''
# Blender v2.82 (sub 7) OBJ File: ''
# www.blender.org
mtllib plane.mtl
o Plane
v -1.000000 0.000000 1.000000
v 1.000000 0.000000 1.000000
v -1.000000 0.000000 -1.000000
v 1.000000 0.000000 -1.000000
vt 1.000000 0.000000
vt 0.000000 1.000000
vt 0.000000 0.000000
vt 1.000000 1.000000
vn 0.0000 1.0000 0.0000
usemtl None
s off
f 2/1/1 3/2/1 1/3/1
f 2/1/1 4/4/1 3/2/1
'''

# Vertex coordinates for a level 0 plane.
_plane_verts0 = [
    [-1.000000, 0.000000, 1.000000],
    [1.000000, 0.000000, 1.000000],
    [-1.000000, 0.000000, -1.000000],
    [1.000000, 0.000000, -1.000000],

]

# Faces for level 0 plane
_plane_faces0 = [
    [1, 2, 0],
    [1, 3, 2]
]

This didn't work either.
My current hacky workaround is to load the plane from Blender:
blender_plane = load_objs_as_meshes(['plane.obj'], device=device)

(Once it's in PyTorch3D's Meshes format I can use SubdivideMeshes as needed.)
I would like to understand what the correct face index winding is for PyTorch3D (so I can potentially define other procedural meshes).

The solution is to remove this line: verts /= verts.norm(p=2, dim=1, keepdim=True)
In more detail:
import torch
from pytorch3d.ops.subdivide_meshes import SubdivideMeshes
from pytorch3d.structures.meshes import Meshes


# Vertex coordinates for a level 0 plane.
_plane_verts0 = [
    [-0.5000, -0.5000, 0.0000],  # TL
    [+0.5000, -0.5000, 0.0000],  # TR
    [+0.5000, +0.5000, 0.0000],  # BR
    [-0.5000, +0.5000, 0.0000],  # BL
]

# Faces for level 0 plane
_plane_faces0 = [[2, 1, 0], [0, 3, 2]]


def plane(level: int = 0, device=None):
    &quot;&quot;&quot;
    Create verts and faces for a unit plane, with all faces oriented
    consistently.
    Args:
        level: integer specifying the number of iterations for subdivision
               of the mesh faces. Each additional level will result in four new
               faces per face.
        device: A torch.device object on which the outputs will be allocated.
    Returns:
        Meshes object with verts and faces.
    &quot;&quot;&quot;
    if device is None:
        device = torch.device(&quot;cpu&quot;)
    if level &lt; 0:
        raise ValueError(&quot;level must be &gt;= 0.&quot;)
    if level == 0:
        verts = torch.tensor(_plane_verts0, dtype=torch.float32, device=device)
        faces = torch.tensor(_plane_faces0, dtype=torch.int64, device=device)
    else:
        mesh = plane(level - 1, device)
        subdivide = SubdivideMeshes()
        mesh = subdivide(mesh)
        verts = mesh.verts_list()[0]
        faces = mesh.faces_list()[0]
    return Meshes(verts=[verts], faces=[faces])

