Repository: adamlwgriffiths/Pyrr

Issue:
generate_normals for vertices instead of faces
Your utility `pyrr.vector3.generate_normals` works wonders for generating face normals. However, for constructing vertex normals from those face normals, I needed to write some additional code.

```python
def generate_vertex_normals(vertices, index):
    face_normals = pyrr.vector3.generate_normals(*np.rollaxis(vertices[index], axis=1))
    vertex_normals = np.zeros_like(vertices)
    for i in range(index.shape[-1]):
        np.add.at(vertex_normals, index[:, i], face_normals)
    return pyrr.vector3.normalize(vertex_normals)
```

Is there already such functionality in the library that I may have missed? Is there an intention to add such a utility function?

Discussion:

Patch:
diff --git a/README.md b/README.md
index 734c277..b005037 100644
--- a/README.md
+++ b/README.md
@@ -188,10 +188,11 @@ Authors
 -------
 
   * [Adam Griffiths](https://github.com/adamlwgriffiths/).
-  * [Chris Bates](https://github.com/chrsbats)
-  * [Jakub Stasiak](https://github.com/jstasiak/).
+  * [Chris Bates](https://github.com/chrsbats/).
+  * [Jakub Stasiak](https://github.com/jstasiak/).
   * [Bogdan Teleaga](https://github.com/bogdanteleaga/).
-  * [Szabolcs Dombi](https://github.com/cprogrammer1994)
+  * [Szabolcs Dombi](https://github.com/cprogrammer1994/).
+  * [Korijn van Golen](https://github.com/Korijn/).
 
 Contributions are welcome.
 
diff --git a/docs/source/info_contributing.rst b/docs/source/info_contributing.rst
index eaf4d9a..136e285 100644
--- a/docs/source/info_contributing.rst
+++ b/docs/source/info_contributing.rst
@@ -25,5 +25,6 @@ Developers and contributors include:
 
     * `Adam Griffiths <https://github.com/adamlwgriffiths/>`_
     * `Jakub Stasiak <https://github.com/jstasiak/>`_
+    * `Korijn van Golen <https://github.com/Korijn/>`_
 
 Is your name left out? Post an issue in `Pyrr's bug tracker <https://github.com/adamlwgriffiths/Pyrr/issues/>`_ =)
diff --git a/pyrr/vector3.py b/pyrr/vector3.py
index 198792e..7e52c84 100644
--- a/pyrr/vector3.py
+++ b/pyrr/vector3.py
@@ -89,9 +89,43 @@ def generate_normals(v1, v2, v3, normalize_result=True):
     b = v3 - v2
     n = cross(b, a)
     if normalize_result:
-        normalize(n)
+        n = normalize(n)
     return n
 
+def generate_vertex_normals(vertices, index, normalize_result=True):
+    """Generates a normal vector for each vertex.
+
+    The result is a normalized vector.
+
+    The index array should list the faces by indexing into the
+    vertices array. It is assumed the ordering in index is
+    counter-clockwise.
+
+    The vertices and index arrays are Nd arrays and must be 2d,
+    where the final axis is of size 3.
+
+    An example::
+        >>> vertices = numpy.array( [ [ 1.0, 0.0, 0.0 ], [ 0.0, 0.0, 0.0 ], [ 0.0, 1.0, 0.0 ] ] )
+        >>> index = numpy.array( [ [ 0, 2, 1 ] ] )
+        >>> vector.generate_vertex_normals( vertices, index )
+        array([[ 0.,  0., 1.], [ 0.,  0., 1.], [ 0.,  0., 1.]])
+
+    :param numpy.array vertices: an 2d array with the final dimension
+        being size 3. (a vector)
+    :param numpy.array index: an Nd array with the final dimension
+        being size 3. (a vector)
+    :param boolean normalize_result: Specifies if the result should
+        be normalized before being returned.
+    """
+    v1, v2, v3 = np.rollaxis(vertices[index], axis=-2)
+    face_normals = generate_normals(v1, v2, v3, normalize_result=False)
+    vertex_normals = np.zeros_like(vertices)
+    for i in range(3):
+        np.add.at(vertex_normals, index[..., i], face_normals)
+    if normalize_result:
+        vertex_normals = normalize(vertex_normals)
+    return vertex_normals
+
 
 class index:
     #: The index of the X value within the vector


Test patch:
diff --git a/pyrr/tests/test_vector3.py b/pyrr/tests/test_vector3.py
index cc64312..a51d489 100644
--- a/pyrr/tests/test_vector3.py
+++ b/pyrr/tests/test_vector3.py
@@ -102,6 +102,85 @@ class test_vector3(unittest.TestCase):
         ]
         np.testing.assert_almost_equal(result, expected, decimal=5)
 
+    def test_generate_normals(self):
+        vertices = np.array([
+            [2.0, 0.0, 0.0],
+            [0.0, 0.0, 0.0],
+            [0.0, 2.0, 0.0],
+            [2.0, 2.0, 0.0]
+        ])
+        index = np.array([
+            [0, 2, 1],
+            [0, 3, 2],
+        ])
+        v1, v2, v3 = np.rollaxis(vertices[index], axis=1)
+        result = vector3.generate_normals(v1, v2, v3)
+        expected = np.array([
+            [0., 0., 1.],
+            [0., 0., 1.]
+        ])
+        np.testing.assert_array_equal(result, expected)
+
+    def test_generate_normals_unnormalized(self):
+        vertices = np.array([
+            [2.0, 0.0, 0.0],
+            [0.0, 0.0, 0.0],
+            [0.0, 2.0, 0.0],
+            [2.0, 2.0, 0.0]
+        ])
+        index = np.array([
+            [0, 2, 1],
+            [0, 3, 2],
+        ])
+        v1, v2, v3 = np.rollaxis(vertices[index], axis=1)
+        result = vector3.generate_normals(v1, v2, v3, normalize_result=False)
+        expected = np.array([
+            [0., 0., 4.],
+            [0., 0., 4.]
+        ])
+        np.testing.assert_array_equal(result, expected)
+
+    def test_generate_vertex_normals(self):
+        vertices = np.array([
+            [1.0, 0.0, 0.0],
+            [0.0, 0.0, 0.0],
+            [0.0, 1.0, 0.0],
+            [1.0, 1.0, 0.0]
+        ])
+        index = np.array([
+            [0, 2, 1],
+            [0, 3, 2],
+        ])
+        result = vector3.generate_vertex_normals(vertices, index)
+        expected = np.array([
+            [0., 0., 1.],
+            [0., 0., 1.],
+            [0., 0., 1.],
+            [0., 0., 1.]
+        ])
+        np.testing.assert_array_equal(result, expected)
+    
+    def test_generate_vertex_normals_unnormalized(self):
+        vertices = np.array([
+            [1.0, 0.0, 0.0],
+            [0.0, 0.0, 0.0],
+            [0.0, 1.0, 0.0],
+            [1.0, 1.0, 0.0]
+        ])
+        index = np.array([
+            [0, 2, 1],
+            [0, 3, 2],
+        ])
+        result = vector3.generate_vertex_normals(
+            vertices, index, normalize_result=False)
+        expected = np.array([
+            [0., 0., 2.],
+            [0., 0., 1.],
+            [0., 0., 2.],
+            [0., 0., 1.]
+        ])
+        np.testing.assert_array_equal(result, expected)
+
     def test_squared_length_single_vector(self):
         result = vector3.squared_length([1.,1.,1.])
         np.testing.assert_almost_equal(result, 3., decimal=5)