| |
| |
| |
| |
| |
|
|
| import unittest |
|
|
| import numpy as np |
| import torch |
| from pytorch3d.renderer.lighting import AmbientLights, DirectionalLights, PointLights |
| from pytorch3d.transforms import RotateAxisAngle |
|
|
| from .common_testing import TestCaseMixin |
|
|
|
|
| class TestLights(TestCaseMixin, unittest.TestCase): |
| def test_init_lights(self): |
| """ |
| Initialize Lights class with the default values. |
| """ |
| device = torch.device("cuda:0") |
| light = DirectionalLights(device=device) |
| keys = ["ambient_color", "diffuse_color", "specular_color", "direction"] |
| for k in keys: |
| prop = getattr(light, k) |
| self.assertTrue(torch.is_tensor(prop)) |
| self.assertTrue(prop.device == device) |
| self.assertTrue(prop.shape == (1, 3)) |
|
|
| light = PointLights(device=device) |
| keys = ["ambient_color", "diffuse_color", "specular_color", "location"] |
| for k in keys: |
| prop = getattr(light, k) |
| self.assertTrue(torch.is_tensor(prop)) |
| self.assertTrue(prop.device == device) |
| self.assertTrue(prop.shape == (1, 3)) |
|
|
| def test_lights_clone_to(self): |
| device = torch.device("cuda:0") |
| cpu = torch.device("cpu") |
| light = DirectionalLights() |
| new_light = light.clone().to(device) |
| keys = ["ambient_color", "diffuse_color", "specular_color", "direction"] |
| for k in keys: |
| prop = getattr(light, k) |
| new_prop = getattr(new_light, k) |
| self.assertTrue(prop.device == cpu) |
| self.assertTrue(new_prop.device == device) |
| self.assertSeparate(new_prop, prop) |
|
|
| light = PointLights() |
| new_light = light.clone().to(device) |
| keys = ["ambient_color", "diffuse_color", "specular_color", "location"] |
| for k in keys: |
| prop = getattr(light, k) |
| new_prop = getattr(new_light, k) |
| self.assertTrue(prop.device == cpu) |
| self.assertTrue(new_prop.device == device) |
| self.assertSeparate(new_prop, prop) |
|
|
| def test_lights_accessor(self): |
| d_light = DirectionalLights(ambient_color=((0.0, 0.0, 0.0), (1.0, 1.0, 1.0))) |
| p_light = PointLights(ambient_color=((0.0, 0.0, 0.0), (1.0, 1.0, 1.0))) |
| for light in [d_light, p_light]: |
| |
| color = (0.5, 0.5, 0.5) |
| light[1].ambient_color = color |
| self.assertClose(light.ambient_color[1], torch.tensor(color)) |
| |
| l0 = light[0] |
| self.assertClose(l0.ambient_color, torch.tensor((0.0, 0.0, 0.0))) |
|
|
| def test_initialize_lights_broadcast(self): |
| light = DirectionalLights( |
| ambient_color=torch.randn(10, 3), |
| diffuse_color=torch.randn(1, 3), |
| specular_color=torch.randn(1, 3), |
| ) |
| keys = ["ambient_color", "diffuse_color", "specular_color", "direction"] |
| for k in keys: |
| prop = getattr(light, k) |
| self.assertTrue(prop.shape == (10, 3)) |
|
|
| light = PointLights( |
| ambient_color=torch.randn(10, 3), |
| diffuse_color=torch.randn(1, 3), |
| specular_color=torch.randn(1, 3), |
| ) |
| keys = ["ambient_color", "diffuse_color", "specular_color", "location"] |
| for k in keys: |
| prop = getattr(light, k) |
| self.assertTrue(prop.shape == (10, 3)) |
|
|
| def test_initialize_lights_broadcast_fail(self): |
| """ |
| Batch dims have to be the same or 1. |
| """ |
| with self.assertRaises(ValueError): |
| DirectionalLights( |
| ambient_color=torch.randn(10, 3), diffuse_color=torch.randn(15, 3) |
| ) |
|
|
| with self.assertRaises(ValueError): |
| PointLights( |
| ambient_color=torch.randn(10, 3), diffuse_color=torch.randn(15, 3) |
| ) |
|
|
| def test_initialize_lights_dimensions_fail(self): |
| """ |
| Color should have shape (N, 3) or (1, 3) |
| """ |
| with self.assertRaises(ValueError): |
| DirectionalLights(ambient_color=torch.randn(10, 4)) |
|
|
| with self.assertRaises(ValueError): |
| DirectionalLights(direction=torch.randn(10, 4)) |
|
|
| with self.assertRaises(ValueError): |
| PointLights(ambient_color=torch.randn(10, 4)) |
|
|
| with self.assertRaises(ValueError): |
| PointLights(location=torch.randn(10, 4)) |
|
|
| def test_initialize_ambient(self): |
| N = 13 |
| color = 0.8 * torch.ones((N, 3)) |
| lights = AmbientLights(ambient_color=color) |
| self.assertEqual(len(lights), N) |
| self.assertClose(lights.ambient_color, color) |
|
|
| lights = AmbientLights(ambient_color=color[:1]) |
| self.assertEqual(len(lights), 1) |
| self.assertClose(lights.ambient_color, color[:1]) |
|
|
|
|
| class TestDiffuseLighting(TestCaseMixin, unittest.TestCase): |
| def test_diffuse_directional_lights(self): |
| """ |
| Test with a single point where: |
| 1) the normal and light direction are 45 degrees apart. |
| 2) the normal and light direction are 90 degrees apart. The output |
| should be zero for this case |
| """ |
| color = torch.tensor([1, 1, 1], dtype=torch.float32) |
| direction = torch.tensor( |
| [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
| normals = torch.tensor([0, 0, 1], dtype=torch.float32) |
| normals = normals[None, None, :] |
| expected_output = torch.tensor( |
| [1 / np.sqrt(2), 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
| expected_output = expected_output.view(1, 1, 3).repeat(3, 1, 1) |
| light = DirectionalLights(diffuse_color=color, direction=direction) |
| output_light = light.diffuse(normals=normals) |
| self.assertClose(output_light, expected_output) |
|
|
| |
| direction = torch.tensor([0, 1, 0], dtype=torch.float32) |
| light.direction = direction |
| expected_output = torch.zeros_like(expected_output) |
| output_light = light.diffuse(normals=normals) |
| self.assertClose(output_light, expected_output) |
|
|
| def test_diffuse_point_lights(self): |
| """ |
| Test with a single point at the origin. Test two cases: |
| 1) the point light is at (1, 0, 1) hence the light direction is 45 |
| degrees apart from the normal direction |
| 1) the point light is at (0, 1, 0) hence the light direction is 90 |
| degrees apart from the normal direction. The output |
| should be zero for this case |
| """ |
| color = torch.tensor([1, 1, 1], dtype=torch.float32) |
| location = torch.tensor( |
| [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
| points = torch.tensor([0, 0, 0], dtype=torch.float32) |
| normals = torch.tensor([0, 0, 1], dtype=torch.float32) |
| expected_output = torch.tensor( |
| [1 / np.sqrt(2), 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
| expected_output = expected_output.view(-1, 1, 3) |
| light = PointLights(diffuse_color=color[None, :], location=location[None, :]) |
| output_light = light.diffuse( |
| points=points[None, None, :], normals=normals[None, None, :] |
| ) |
| self.assertClose(output_light, expected_output) |
|
|
| |
| location = torch.tensor([0, 1, 0], dtype=torch.float32) |
| expected_output = torch.zeros_like(expected_output) |
| light = PointLights(diffuse_color=color[None, :], location=location[None, :]) |
| output_light = light.diffuse( |
| points=points[None, None, :], normals=normals[None, None, :] |
| ) |
| self.assertClose(output_light, expected_output) |
|
|
| def test_diffuse_batched(self): |
| """ |
| Test with a batch where each batch element has one point |
| where the normal and light direction are 45 degrees apart. |
| """ |
| batch_size = 10 |
| color = torch.tensor([1, 1, 1], dtype=torch.float32) |
| direction = torch.tensor( |
| [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
| normals = torch.tensor([0, 0, 1], dtype=torch.float32) |
| expected_out = torch.tensor( |
| [1 / np.sqrt(2), 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
|
|
| |
| direction = direction.view(-1, 3).expand(batch_size, -1) |
| normals = normals.view(-1, 1, 3).expand(batch_size, -1, -1) |
| color = color.view(-1, 3).expand(batch_size, -1) |
| expected_out = expected_out.view(-1, 1, 3).expand(batch_size, 1, 3) |
|
|
| lights = DirectionalLights(diffuse_color=color, direction=direction) |
| output_light = lights.diffuse(normals=normals) |
| self.assertClose(output_light, expected_out) |
|
|
| def test_diffuse_batched_broadcast_inputs(self): |
| """ |
| Test with a batch where each batch element has one point |
| where the normal and light direction are 45 degrees apart. |
| The color and direction are the same for each batch element. |
| """ |
| batch_size = 10 |
| color = torch.tensor([1, 1, 1], dtype=torch.float32) |
| direction = torch.tensor( |
| [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
| normals = torch.tensor([0, 0, 1], dtype=torch.float32) |
| expected_out = torch.tensor( |
| [1 / np.sqrt(2), 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32 |
| ) |
|
|
| |
| normals = normals.view(-1, 1, 3).expand(batch_size, -1, -1) |
| expected_out = expected_out.view(-1, 1, 3).expand(batch_size, 1, 3) |
|
|
| |
| |
| direction = direction.view(1, 3) |
| color = color.view(1, 3) |
|
|
| lights = DirectionalLights(diffuse_color=color, direction=direction) |
| output_light = lights.diffuse(normals=normals) |
| self.assertClose(output_light, expected_out) |
|
|
| def test_diffuse_batched_arbitrary_input_dims(self): |
| """ |
| Test with a batch of inputs where shape of the input is mimicking the |
| shape in a shading function i.e. an interpolated normal per pixel for |
| top K faces per pixel. |
| """ |
| N, H, W, K = 16, 256, 256, 100 |
| device = torch.device("cuda:0") |
| color = torch.tensor([1, 1, 1], dtype=torch.float32, device=device) |
| direction = torch.tensor( |
| [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], dtype=torch.float32, device=device |
| ) |
| normals = torch.tensor([0, 0, 1], dtype=torch.float32, device=device) |
| normals = normals.view(1, 1, 1, 1, 3).expand(N, H, W, K, -1) |
| direction = direction.view(1, 3) |
| color = color.view(1, 3) |
| expected_output = torch.tensor( |
| [1 / np.sqrt(2), 1 / np.sqrt(2), 1 / np.sqrt(2)], |
| dtype=torch.float32, |
| device=device, |
| ) |
| expected_output = expected_output.view(1, 1, 1, 1, 3) |
| expected_output = expected_output.expand(N, H, W, K, -1) |
|
|
| lights = DirectionalLights(diffuse_color=color, direction=direction) |
| output_light = lights.diffuse(normals=normals) |
| self.assertClose(output_light, expected_output) |
|
|
| def test_diffuse_batched_packed(self): |
| """ |
| Test with a batch of 2 meshes each of which has faces on a single plane. |
| The normal and light direction are 45 degrees apart for the first mesh |
| and 90 degrees apart for the second mesh. |
| |
| The points and normals are in the packed format i.e. no batch dimension. |
| """ |
| verts_packed = torch.rand((10, 3)) |
| faces_per_mesh = [6, 4] |
| mesh_to_vert_idx = [0] * faces_per_mesh[0] + [1] * faces_per_mesh[1] |
| mesh_to_vert_idx = torch.tensor(mesh_to_vert_idx, dtype=torch.int64) |
| color = torch.tensor([[1, 1, 1], [1, 1, 1]], dtype=torch.float32) |
| direction = torch.tensor( |
| [ |
| [0, 1 / np.sqrt(2), 1 / np.sqrt(2)], |
| [0, 1, 0], |
| ], |
| dtype=torch.float32, |
| ) |
| normals = torch.tensor([[0, 0, 1], [0, 0, 1]], dtype=torch.float32) |
| expected_output = torch.zeros_like(verts_packed, dtype=torch.float32) |
| expected_output[:6, :] += 1 / np.sqrt(2) |
| expected_output[6:, :] = 0.0 |
| lights = DirectionalLights( |
| diffuse_color=color[mesh_to_vert_idx, :], |
| direction=direction[mesh_to_vert_idx, :], |
| ) |
| output_light = lights.diffuse(normals=normals[mesh_to_vert_idx, :]) |
| self.assertClose(output_light, expected_output) |
|
|
|
|
| class TestSpecularLighting(TestCaseMixin, unittest.TestCase): |
| def test_specular_directional_lights(self): |
| """ |
| Specular highlights depend on the camera position as well as the light |
| position/direction. |
| Test with a single point where: |
| 1) the normal and light direction are -45 degrees apart and the normal |
| and camera position are +45 degrees apart. The reflected light ray |
| will be perfectly aligned with the camera so the output is 1.0. |
| 2) the normal and light direction are -45 degrees apart and the |
| camera position is behind the point. The output should be zero for |
| this case. |
| """ |
| color = torch.tensor([1, 0, 1], dtype=torch.float32) |
| direction = torch.tensor( |
| [-1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| points = torch.tensor([0, 0, 0], dtype=torch.float32) |
| normals = torch.tensor([0, 1, 0], dtype=torch.float32) |
| expected_output = torch.tensor([1.0, 0.0, 1.0], dtype=torch.float32) |
| expected_output = expected_output.view(1, 1, 3).repeat(3, 1, 1) |
| lights = DirectionalLights(specular_color=color, direction=direction) |
| output_light = lights.specular( |
| points=points[None, None, :], |
| normals=normals[None, None, :], |
| camera_position=camera_position[None, :], |
| shininess=torch.tensor(10), |
| ) |
| self.assertClose(output_light, expected_output) |
|
|
| |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), -1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| expected_output = torch.zeros_like(expected_output) |
| output_light = lights.specular( |
| points=points[None, None, :], |
| normals=normals[None, None, :], |
| camera_position=camera_position[None, :], |
| shininess=torch.tensor(10), |
| ) |
| self.assertClose(output_light, expected_output) |
|
|
| def test_specular_point_lights(self): |
| """ |
| Replace directional lights with point lights and check the output |
| is the same. |
| |
| Test an additional case where the angle between the light reflection |
| direction and the view direction is 30 degrees. |
| """ |
| color = torch.tensor([1, 0, 1], dtype=torch.float32) |
| location = torch.tensor([-1, 1, 0], dtype=torch.float32) |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| points = torch.tensor([0, 0, 0], dtype=torch.float32) |
| normals = torch.tensor([0, 1, 0], dtype=torch.float32) |
| expected_output = torch.tensor([1.0, 0.0, 1.0], dtype=torch.float32) |
| expected_output = expected_output.view(-1, 1, 3) |
| lights = PointLights(specular_color=color[None, :], location=location[None, :]) |
| output_light = lights.specular( |
| points=points[None, None, :], |
| normals=normals[None, None, :], |
| camera_position=camera_position[None, :], |
| shininess=torch.tensor(10), |
| ) |
| self.assertClose(output_light, expected_output) |
|
|
| |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), -1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| expected_output = torch.zeros_like(expected_output) |
| output_light = lights.specular( |
| points=points[None, None, :], |
| normals=normals[None, None, :], |
| camera_position=camera_position[None, :], |
| shininess=torch.tensor(10), |
| ) |
| self.assertClose(output_light, expected_output) |
|
|
| |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| rotate_30 = RotateAxisAngle(-30, axis="z") |
| camera_position = rotate_30.transform_points(camera_position[None, :]) |
| expected_output = torch.tensor( |
| [np.cos(30.0 * np.pi / 180), 0.0, np.cos(30.0 * np.pi / 180)], |
| dtype=torch.float32, |
| ) |
| expected_output = expected_output.view(-1, 1, 3) |
| output_light = lights.specular( |
| points=points[None, None, :], |
| normals=normals[None, None, :], |
| camera_position=camera_position[None, :], |
| shininess=torch.tensor(10), |
| ) |
| self.assertClose(output_light, expected_output**10) |
|
|
| def test_specular_batched(self): |
| batch_size = 10 |
| color = torch.tensor([1, 0, 1], dtype=torch.float32) |
| direction = torch.tensor( |
| [-1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| points = torch.tensor([0, 0, 0], dtype=torch.float32) |
| normals = torch.tensor([0, 1, 0], dtype=torch.float32) |
| expected_out = torch.tensor([1.0, 0.0, 1.0], dtype=torch.float32) |
|
|
| |
| direction = direction.view(1, 3).expand(batch_size, -1) |
| camera_position = camera_position.view(1, 3).expand(batch_size, -1) |
| normals = normals.view(1, 1, 3).expand(batch_size, -1, -1) |
| points = points.view(1, 1, 3).expand(batch_size, -1, -1) |
| color = color.view(1, 3).expand(batch_size, -1) |
| expected_out = expected_out.view(1, 1, 3).expand(batch_size, 1, 3) |
|
|
| lights = DirectionalLights(specular_color=color, direction=direction) |
| output_light = lights.specular( |
| points=points, |
| normals=normals, |
| camera_position=camera_position, |
| shininess=torch.tensor(10), |
| ) |
| self.assertClose(output_light, expected_out) |
|
|
| def test_specular_batched_broadcast_inputs(self): |
| batch_size = 10 |
| color = torch.tensor([1, 0, 1], dtype=torch.float32) |
| direction = torch.tensor( |
| [-1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| points = torch.tensor([0, 0, 0], dtype=torch.float32) |
| normals = torch.tensor([0, 1, 0], dtype=torch.float32) |
| expected_out = torch.tensor([1.0, 0.0, 1.0], dtype=torch.float32) |
|
|
| |
| normals = normals.view(1, 1, 3).expand(batch_size, -1, -1) |
| points = points.view(1, 1, 3).expand(batch_size, -1, -1) |
| expected_out = expected_out.view(1, 1, 3).expand(batch_size, 1, 3) |
|
|
| |
| |
| direction = direction.view(1, 3) |
| camera_position = camera_position.view(1, 3) |
| color = color.view(1, 3) |
|
|
| lights = DirectionalLights(specular_color=color, direction=direction) |
| output_light = lights.specular( |
| points=points, |
| normals=normals, |
| camera_position=camera_position, |
| shininess=torch.tensor(10), |
| ) |
| self.assertClose(output_light, expected_out) |
|
|
| def test_specular_batched_arbitrary_input_dims(self): |
| """ |
| Test with a batch of inputs where shape of the input is mimicking the |
| shape expected after rasterization i.e. a normal per pixel for |
| top K faces per pixel. |
| """ |
| device = torch.device("cuda:0") |
| N, H, W, K = 8, 128, 128, 100 |
| color = torch.tensor([1, 0, 1], dtype=torch.float32, device=device) |
| direction = torch.tensor( |
| [-1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| camera_position = torch.tensor( |
| [+1 / np.sqrt(2), 1 / np.sqrt(2), 0], dtype=torch.float32 |
| ) |
| points = torch.tensor([0, 0, 0], dtype=torch.float32, device=device) |
| normals = torch.tensor([0, 1, 0], dtype=torch.float32, device=device) |
| points = points.view(1, 1, 1, 1, 3).expand(N, H, W, K, 3) |
| normals = normals.view(1, 1, 1, 1, 3).expand(N, H, W, K, 3) |
|
|
| direction = direction.view(1, 3) |
| color = color.view(1, 3) |
| camera_position = camera_position.view(1, 3) |
|
|
| expected_output = torch.tensor( |
| [1.0, 0.0, 1.0], dtype=torch.float32, device=device |
| ) |
| expected_output = expected_output.view(-1, 1, 1, 1, 3) |
| expected_output = expected_output.expand(N, H, W, K, -1) |
|
|
| lights = DirectionalLights(specular_color=color, direction=direction) |
| output_light = lights.specular( |
| points=points, |
| normals=normals, |
| camera_position=camera_position, |
| shininess=10.0, |
| ) |
| self.assertClose(output_light, expected_output) |
|
|
| def test_specular_batched_packed(self): |
| """ |
| Test with a batch of 2 meshes each of which has faces on a single plane. |
| The points and normals are in the packed format i.e. no batch dimension. |
| """ |
| faces_per_mesh = [6, 4] |
| mesh_to_vert_idx = [0] * faces_per_mesh[0] + [1] * faces_per_mesh[1] |
| mesh_to_vert_idx = torch.tensor(mesh_to_vert_idx, dtype=torch.int64) |
| color = torch.tensor([[1, 1, 1], [1, 0, 1]], dtype=torch.float32) |
| direction = torch.tensor( |
| [[-1 / np.sqrt(2), 1 / np.sqrt(2), 0], [-1, 1, 0]], dtype=torch.float32 |
| ) |
| camera_position = torch.tensor( |
| [ |
| [+1 / np.sqrt(2), 1 / np.sqrt(2), 0], |
| [+1 / np.sqrt(2), -1 / np.sqrt(2), 0], |
| ], |
| dtype=torch.float32, |
| ) |
| points = torch.tensor([[0, 0, 0]], dtype=torch.float32) |
| normals = torch.tensor([[0, 1, 0], [0, 1, 0]], dtype=torch.float32) |
| expected_output = torch.zeros((10, 3), dtype=torch.float32) |
| expected_output[:6, :] += 1.0 |
|
|
| lights = DirectionalLights( |
| specular_color=color[mesh_to_vert_idx, :], |
| direction=direction[mesh_to_vert_idx, :], |
| ) |
| output_light = lights.specular( |
| points=points.view(-1, 3).expand(10, -1), |
| normals=normals.view(-1, 3)[mesh_to_vert_idx, :], |
| camera_position=camera_position[mesh_to_vert_idx, :], |
| shininess=10.0, |
| ) |
| self.assertClose(output_light, expected_output) |
|
|