1

I'm using vispy to render 3D parametric surfaces with OpenGL. It seems to be working well for the most part but it looks weird

in some parts around the edges of the geometry, almost as if the normals were pointing inwards (example).

I suspect the problem lies either with my fragment shader or the surface normals, especially in areas where the they are close to perpendicular to the camera direction. Here's how the vertices are created:

# --- Python imports ---
import numpy as np

# --- Internal imports ---
from glmesh import MeshApp3D

# -----------------------------------------------------------
# Torus parameters
radius1     = 1.0
radius2     = 0.2

# Define torus
torus       = ( lambda u,v : np.cos(u)*(radius1+radius2*np.cos(v)),
                lambda u,v : np.sin(u)*(radius1+radius2*np.cos(v)),
                lambda u,v : radius2*np.sin(v) )

# Generate grid of points on the torus
nSamples    = 100
U           = np.linspace(0.0,2*np.pi,nSamples)
V           = np.linspace(0.0,-2*np.pi,nSamples)

grid        = np.array( 
                        [
                            [ [torus[componentIndex](u,v) for u in U] for v in V ] for componentIndex in (0,1,2)
                        ],
                        dtype=np.float32 )


# -----------------------------------------------------------
# Rearrange grid to a list of points
vertices    = np.reshape(
                        np.transpose(   grid, 
                                        axes=(1,2,0)),
                        ( nSamples*nSamples ,3))

# Generate triangle indices in a positive permutation
# For example:
# 7---8---9
# | / | / |
# 4---5---6
# | / | / |
# 1---2---3
#
# (1,5,4) (1,2,5) (2,6,5) (2,3,6) (4,8,7) (4,5,8) (5,9,8) (5,6,9)

faces = np.zeros( ( 2*(nSamples-1)*(nSamples-1), 3 ), dtype=np.uint32 )
k = 0
for j in range(nSamples-1):
    for i in range(nSamples-1):
        jni = j*nSamples+i

        faces[k]    = [ jni, 
                        jni+nSamples+1, 
                        jni+nSamples ]

        faces[k+1]  = [ jni, 
                        jni+1, 
                        jni+1+nSamples ]
        k+=2


# -----------------------------------------------------------
# -----------------------------------------------------------
meshApp = MeshApp3D(    {'vertices' : vertices, 'faces' : faces},
                        colors=(1.0,1.0,1.0) )
meshApp.run()

The fragment shader:

varying vec3 position;
varying vec3 normal;
varying vec4 color;


void main() {

    // Diffuse
    vec3 lightDir       = normalize( position - vec3($lightPos) );
    float diffuse       = dot( normal, lightDir );
    //diffuse             = min( max(diffuse,0.0), 1.0 );

    // Specular
    vec3 halfWayVector  = normalize(  ( lightDir+vec3($cameraDir) )/2.0  );
    float specular      = min( max(   dot(halfWayVector,normal)   ,0.0), 1.0 );
    specular            = specular * specular * specular * specular;
    specular            = specular * specular * specular * specular;
    specular            = specular * specular * specular * specular;

    // Additive
    vec3 fragColor      = vec3($lightColor) * color.xyz;
    fragColor           = fragColor * ( $diffuseMaterialConstant * diffuse +
                                        $specularMaterialConstant * specular)
                                        + $ambientMaterialConstant * vec3($ambientLight) * color.xyz;


    gl_FragColor        = vec4(fragColor,1.0);
}

The rest of the code is broken down in modules but in a nutshell, here's what happens:

  • a MeshData is constructed from the vertices and faces
  • the normals are generated in MeshData internally, based on the vertex positions and the order in which they define their corresponding triangles
  • a canvas and an OpenGL window is created and initialized
  • the mesh is drawn as triangle strips

A problematic part can be the definition of the mesh triangles. Example:

  7---8---9
  | / | / |
  4---5---6
  | / | / |
  1---2---3
v
^
|
---> u
(1,5,4) (1,2,5) (2,6,5) (2,3,6) (4,8,7) (4,5,8) (5,9,8) (5,6,9)

It was originally meant to generate triangles for 2D functions z=f(x,y) and worked well for them, but I ran into problems when applying it to parametric surfaces r(u,v) = [ x(u,v), y(u,v), z(u,v) ]. For example, this is the reason why the parameter v goes from 0 to -2pi instead of +2pi (otherwise the normals would be pointing inwards).

Any ideas on what might be causing the glitches seen in the image?

(or any advice on better triangle generation?)

Rabbid76
  • 202,892
  • 27
  • 131
  • 174
Kelemen Máté
  • 442
  • 3
  • 12
  • One way to check for consistent triangle winding is to enable backface culling. If the dark triangles disappear, then you know you've oriented them backwards. I think this is how you enable culling with vispy: http://vispy.org/gloo.html#vispy.gloo.wrappers.set_cull_face – prideout Nov 10 '19 at 17:32
  • I think we might be interested in how you're generating normals, and your vertex shader. I also wonder if your triangles are simply too small, so the cross-product that you're presumably doing is degenerate sometimes. – prideout Nov 10 '19 at 17:39
  • 1
    @prideout Thanks for the hint on culling, it helped me trace the problem. By default, culling is set to 'back' faces in vispy. Changing it to 'front' resulted in the same case as the second picture shows in the original post (with the purposefully inverted normals). You can see that even though the triangles are showing their backs, they aren't getting culled. The reason was that some triangles were duplicated: Triangle drawing mode was set to 'triangle_strip' instead of 'triangles', which created some duplicates. Setting drawing mode to 'triangles' solved everything. – Kelemen Máté Nov 11 '19 at 14:03
  • @KelemenMáté Glad you got this working. If there is anything you think could be contributed or made easier in the vispy library let me know by creating an issue on the vispy github repository. Thanks. – djhoese Oct 03 '20 at 11:52

0 Answers0