diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 1ed09711c..62933565a 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -48,6 +48,7 @@ pybind11_add_module(sdformat SHARED src/sdf/pyGeometry.cc src/sdf/pyJoint.cc src/sdf/pyJointAxis.cc + src/sdf/pyLight.cc src/sdf/pyLink.cc src/sdf/pyMaterial.cc src/sdf/pyMesh.cc @@ -90,6 +91,7 @@ if (BUILD_TESTING) pyGeometry_TEST pyJoint_TEST pyJointAxis_TEST + pyLight_TEST pyLink_TEST pyMaterial_TEST pyMesh_TEST diff --git a/python/src/sdf/_ignition_sdformat_pybind11.cc b/python/src/sdf/_ignition_sdformat_pybind11.cc index f5dc86b69..334137c7e 100644 --- a/python/src/sdf/_ignition_sdformat_pybind11.cc +++ b/python/src/sdf/_ignition_sdformat_pybind11.cc @@ -26,6 +26,7 @@ #include "pyGeometry.hh" #include "pyJoint.hh" #include "pyJointAxis.hh" +#include "pyLight.hh" #include "pyLink.hh" #include "pyMaterial.hh" #include "pyMesh.hh" @@ -54,6 +55,7 @@ PYBIND11_MODULE(sdformat, m) { sdf::python::defineGeometry(m); sdf::python::defineJoint(m); sdf::python::defineJointAxis(m); + sdf::python::defineLight(m); sdf::python::defineLink(m); sdf::python::defineMaterial(m); sdf::python::defineMesh(m); diff --git a/python/src/sdf/pyLight.cc b/python/src/sdf/pyLight.cc new file mode 100644 index 000000000..686ff6d7c --- /dev/null +++ b/python/src/sdf/pyLight.cc @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "pyLight.hh" + +#include + +#include "sdf/ParserConfig.hh" + +#include "sdf/Box.hh" +#include "sdf/Capsule.hh" +#include "sdf/Cylinder.hh" +#include "sdf/Ellipsoid.hh" +#include "sdf/Light.hh" +#include "sdf/Mesh.hh" +#include "sdf/Plane.hh" +#include "sdf/Sphere.hh" + +using namespace pybind11::literals; + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ +///////////////////////////////////////////////// +void defineLight(pybind11::object module) +{ + pybind11::class_ lightModule(module, "Light"); + lightModule + .def(pybind11::init<>()) + .def(pybind11::init()) + .def("type", &sdf::Light::Type, + "Get the type of light.") + .def("set_type", &sdf::Light::SetType, + "Set the type of light.") + .def("name", &sdf::Light::Name, + "Get the name of the light.") + .def("set_name", &sdf::Light::SetName, + "Set the name of the light.") + .def("raw_pose", &sdf::Light::RawPose, + "Get the pose of the camer. This is the pose of the Light " + "as specified in SDF ( ... ).") + .def("set_raw_pose", &sdf::Light::SetRawPose, + "Set the pose of the Light.") + .def("pose_relative_to", &sdf::Light::PoseRelativeTo, + "Get the name of the coordinate frame relative to which this " + "object's pose is expressed. An empty value indicates that the frame " + "is relative to the parent link.") + .def("set_pose_relative_to", &sdf::Light::SetPoseRelativeTo, + "Set the name of the coordinate frame relative to which this " + "object's pose is expressed. An empty value indicates that the frame " + "is relative to the parent link.") + .def("semantic_pose", &sdf::Light::SemanticPose, + "Get SemanticPose object of this object to aid in resolving " + "poses.") + .def("cast_shadows", &sdf::Light::CastShadows, + "Get whether the light casts shadows.") + .def("set_cast_shadows", &sdf::Light::SetCastShadows, + "Set whether the light casts shadows.") + .def("light_on", &sdf::Light::LightOn, + "Get if the light is on") + .def("set_light_on", &sdf::Light::SetLightOn, + "Set if the light is ON/OFF") + .def("visualize", &sdf::Light::Visualize, + "Whether light visualization in the GUI is enabled.") + .def("set_visualize", &sdf::Light::SetVisualize, + "Set whether light visualization in the GUI is enabled.") + .def("intensity", &sdf::Light::Intensity, + "Get the light intensity") + .def("set_intensity", &sdf::Light::SetIntensity, + "Set the light intensity") + .def("diffuse", &sdf::Light::Diffuse, + "Get the diffuse color. The diffuse color is " + "specified by a set of three numbers representing red/green/blue, " + "each in the range of [0,1].") + .def("set_diffuse", &sdf::Light::SetDiffuse, + "Set the diffuse color. The diffuse color is " + "specified by a set of three numbers representing red/green/blue, " + "each in the range of [0,1].") + .def("specular", &sdf::Light::Specular, + "Get the specular color. The specular color is " + "specified by a set of three numbers representing red/green/blue, " + "each in the range of [0,1].") + .def("set_specular", &sdf::Light::SetSpecular, + "Set the specular color. The specular color is " + "specified by a set of three numbers representing red/green/blue, " + "each in the range of [0,1].") + .def("attenuation_range", &sdf::Light::AttenuationRange, + "Get the range of the light source in meters.") + .def("set_attenuation_range", &sdf::Light::SetAttenuationRange, + "Set the range of the light source in meters.") + .def("linear_attenuation_factor", &sdf::Light::LinearAttenuationFactor, + "Get the linear attenuation factor. This value is clamped to " + "a value between 0 and 1, where 1 means attenuate evenly over the " + "distance.") + .def("set_linear_attenuation_factor", + &sdf::Light::SetLinearAttenuationFactor, + "Set the linear attenuation factor. This value is clamped to " + "a value between 0 and 1, where 1 means attenuate evenly over the " + "distance.") + .def("constant_attenuation_factor", &sdf::Light::ConstantAttenuationFactor, + "Get the constant attenuation factor. This value is clamped to " + "a value between 0 and 1, where 1.0 means never attenuate and 0.0 is " + "complete attenutation.") + .def("set_constant_attenuation_factor", + &sdf::Light::SetConstantAttenuationFactor, + "Get the constant attenuation factor. This value is clamped to " + "a value between 0 and 1, where 1.0 means never attenuate and 0.0 is " + "complete attenutation.") + .def("quadratic_attenuation_factor", &sdf::Light::QuadraticAttenuationFactor, + "Get the quadratic attenuation factor which adds a curvature to " + "the attenuation.") + .def("set_quadratic_attenuation_factor", + &sdf::Light::SetQuadraticAttenuationFactor, + "Set the quadratic attenuation factor which adds a curvature to " + "the attenuation.") + .def("direction", &sdf::Light::Direction, + "Get the direction of the light source. This only has meaning " + "for spot and directional light types. The default value is " + "[0, 0, -1].") + .def("set_direction", &sdf::Light::SetDirection, + "Set the direction of the light source. This only has meaning " + "for spot and directional light types.") + .def("spot_inner_angle", &sdf::Light::SpotInnerAngle, + "Get the angle covered by the bright inner cone.") + .def("set_spot_inner_angle", &sdf::Light::SetSpotInnerAngle, + "Set the angle covered by the bright inner cone.") + .def("spot_outer_angle", &sdf::Light::SpotOuterAngle, + "Get the angle covered by the outer cone.") + .def("set_spot_outer_angle", &sdf::Light::SetSpotOuterAngle, + "Set the angle covered by the bright inner cone.") + .def("spot_falloff", &sdf::Light::SpotFalloff, + "Get the rate of falloff between the inner and outer cones. " + "A value of 1.0 is a linear falloff, less than 1.0 is a slower " + "falloff, and a higher value indicates a faster falloff.") + .def("set_spot_falloff", &sdf::Light::SetSpotFalloff, + "Set the rate of falloff between the inner and outer cones. " + "A value of 1.0 is a linear falloff, less than 1.0 is a slower " + "falloff, and a higher value indicates a faster falloff.") + .def("__copy__", [](const sdf::Light &self) { + return sdf::Light(self); + }) + .def("__deepcopy__", [](const sdf::Light &self, pybind11::dict) { + return sdf::Light(self); + }, "memo"_a); + + pybind11::enum_(lightModule, "LightType") + .value("INVALID", sdf::LightType::INVALID) + .value("POINT", sdf::LightType::POINT) + .value("SPOT", sdf::LightType::SPOT) + .value("DIRECTIONAL", sdf::LightType::DIRECTIONAL); +} +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf diff --git a/python/src/sdf/pyLight.hh b/python/src/sdf/pyLight.hh new file mode 100644 index 000000000..a99e6624f --- /dev/null +++ b/python/src/sdf/pyLight.hh @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef SDFORMAT_PYTHON_LIGHT_HH_ +#define SDFORMAT_PYTHON_LIGHT_HH_ + +#include + +#include "sdf/Light.hh" + +#include "sdf/config.hh" + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ +/// Define a pybind11 wrapper for an sdf::Light +/** + * \param[in] module a pybind11 module to add the definition to + */ +void defineLight(pybind11::object module); +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf + +#endif // SDFORMAT_PYTHON_LIGHT_HH_ diff --git a/python/test/pyLight_TEST.py b/python/test/pyLight_TEST.py new file mode 100644 index 000000000..35f1cbebe --- /dev/null +++ b/python/test/pyLight_TEST.py @@ -0,0 +1,224 @@ +# Copyright (C) 2022 Open Source Robotics Foundation + +# Licensed under the Apache License, Version 2.0 (the "License") +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +from ignition.math import Angle, Color, Pose3d, Vector3d +from sdformat import Light +import math +import unittest + + +class LightColor(unittest.TestCase): + + def test_default_construction(self): + light = Light() + self.assertEqual(Light.LightType.POINT, light.type()) + self.assertFalse(light.name()) + + light.set_name("test_light") + self.assertEqual("test_light", light.name()) + + self.assertEqual(Pose3d.ZERO, light.raw_pose()) + self.assertFalse(light.pose_relative_to()) + + semanticPose = light.semantic_pose() + self.assertEqual(light.raw_pose(), semanticPose.raw_pose()) + self.assertFalse(semanticPose.relative_to()) + pose = Pose3d() + # expect errors when trying to resolve pose + self.assertEqual(1, len(semanticPose.resolve(pose))) + + light.set_raw_pose(Pose3d(1, 2, 3, 0, 0, math.pi)) + self.assertEqual(Pose3d(1, 2, 3, 0, 0, math.pi), light.raw_pose()) + + light.set_pose_relative_to("world") + self.assertEqual("world", light.pose_relative_to()) + + semanticPose = light.semantic_pose() + self.assertEqual(light.raw_pose(), semanticPose.raw_pose()) + self.assertEqual("world", semanticPose.relative_to()) + pose = Pose3d() + # expect errors when trying to resolve pose + self.assertTrue(1, len(semanticPose.resolve(pose))) + + self.assertTrue(light.light_on()) + light.set_light_on(False) + self.assertFalse(light.light_on()) + + self.assertTrue(light.visualize()) + light.set_visualize(False) + self.assertFalse(light.visualize()) + + self.assertFalse(light.cast_shadows()) + light.set_cast_shadows(True) + self.assertTrue(light.cast_shadows()) + + self.assertEqual(Color(0, 0, 0, 1), light.diffuse()) + light.set_diffuse(Color(0.1, 0.2, 0.3, 1.0)) + self.assertEqual(Color(0.1, 0.2, 0.3, 1), light.diffuse()) + + self.assertEqual(Color(0, 0, 0, 1), light.specular()) + light.set_specular(Color(0.4, 0.6, 0.7, 1.0)) + self.assertEqual(Color(0.4, 0.6, 0.7, 1), light.specular()) + + self.assertAlmostEqual(10.0, light.attenuation_range()) + light.set_attenuation_range(1.2) + self.assertAlmostEqual(1.2, light.attenuation_range()) + + self.assertAlmostEqual(1.0, light.linear_attenuation_factor()) + light.set_linear_attenuation_factor(0.2) + self.assertAlmostEqual(0.2, light.linear_attenuation_factor()) + + self.assertAlmostEqual(1.0, light.constant_attenuation_factor()) + light.set_constant_attenuation_factor(0.4) + self.assertAlmostEqual(0.4, light.constant_attenuation_factor()) + + self.assertAlmostEqual(0.0, light.quadratic_attenuation_factor()) + light.set_quadratic_attenuation_factor(1.1) + self.assertAlmostEqual(1.1, light.quadratic_attenuation_factor()) + + self.assertEqual(Vector3d(0, 0, -1), light.direction()) + light.set_direction(Vector3d(0.4, 0.2, 0)) + self.assertEqual(Vector3d(0.4, 0.2, 0), light.direction()) + + self.assertEqual(Angle(0.0), light.spot_inner_angle()) + light.set_spot_inner_angle(Angle(1.4)) + self.assertEqual(Angle(1.4), light.spot_inner_angle()) + + self.assertEqual(Angle(0.0), light.spot_outer_angle()) + light.set_spot_outer_angle(Angle(0.2)) + self.assertEqual(Angle(0.2), light.spot_outer_angle()) + + self.assertAlmostEqual(0.0, light.spot_falloff()) + light.set_spot_falloff(4.3) + self.assertAlmostEqual(4.3, light.spot_falloff()) + + self.assertAlmostEqual(1.0, light.intensity()) + light.set_intensity(0.3) + self.assertAlmostEqual(0.3, light.intensity()) + + def test_copy_construction(self): + light = Light() + light.set_name("test_copy_light") + light.set_type(Light.LightType.DIRECTIONAL) + light.set_raw_pose(Pose3d(3, 2, 1, 0, math.pi, 0)) + light.set_pose_relative_to("ground_plane") + light.set_cast_shadows(True) + light.set_light_on(False) + light.set_visualize(False) + light.set_diffuse(Color(0.4, 0.5, 0.6, 1.0)) + light.set_specular(Color(0.8, 0.9, 0.1, 1.0)) + light.set_attenuation_range(3.2) + light.set_linear_attenuation_factor(0.1) + light.set_constant_attenuation_factor(0.5) + light.set_quadratic_attenuation_factor(0.01) + light.set_direction(Vector3d(0.1, 0.2, 1)) + light.set_spot_inner_angle(Angle(1.9)) + light.set_spot_outer_angle(Angle(3.3)) + light.set_spot_falloff(0.9) + light.set_intensity(1.7) + + light2 = Light(light) + self.assertEqual("test_copy_light", light2.name()) + self.assertEqual(Light.LightType.DIRECTIONAL, light2.type()) + self.assertEqual(Pose3d(3, 2, 1, 0, math.pi, 0), light2.raw_pose()) + self.assertEqual("ground_plane", light2.pose_relative_to()) + self.assertTrue(light2.cast_shadows()) + self.assertFalse(light2.light_on()) + self.assertFalse(light2.visualize()) + self.assertEqual(Color(0.4, 0.5, 0.6, 1), light2.diffuse()) + self.assertEqual(Color(0.8, 0.9, 0.1, 1), light2.specular()) + self.assertAlmostEqual(3.2, light2.attenuation_range()) + self.assertAlmostEqual(0.1, light2.linear_attenuation_factor()) + self.assertAlmostEqual(0.5, light2.constant_attenuation_factor()) + self.assertAlmostEqual(0.01, light2.quadratic_attenuation_factor()) + self.assertEqual(Vector3d(0.1, 0.2, 1), light2.direction()) + self.assertEqual(Angle(1.9), light2.spot_inner_angle()) + self.assertEqual(Angle(3.3), light2.spot_outer_angle()) + self.assertAlmostEqual(0.9, light2.spot_falloff()) + self.assertAlmostEqual(1.7, light2.intensity()) + + def test_deepcopy(self): + light = Light() + light.set_name("test_light_assignment") + light.set_type(Light.LightType.DIRECTIONAL) + light.set_raw_pose(Pose3d(3, 2, 1, 0, math.pi, 0)) + light.set_pose_relative_to("ground_plane") + light.set_cast_shadows(True) + light.set_light_on(False) + light.set_visualize(False) + light.set_diffuse(Color(0.4, 0.5, 0.6, 1.0)) + light.set_specular(Color(0.8, 0.9, 0.1, 1.0)) + light.set_attenuation_range(3.2) + light.set_linear_attenuation_factor(0.1) + light.set_constant_attenuation_factor(0.5) + light.set_quadratic_attenuation_factor(0.01) + light.set_direction(Vector3d(0.1, 0.2, 1)) + light.set_spot_inner_angle(Angle(1.9)) + light.set_spot_outer_angle(Angle(3.3)) + light.set_spot_falloff(0.9) + light.set_intensity(1.7) + + light2 = copy.deepcopy(light) + self.assertEqual("test_light_assignment", light2.name()) + self.assertEqual(Light.LightType.DIRECTIONAL, light2.type()) + self.assertEqual(Pose3d(3, 2, 1, 0, math.pi, 0), light2.raw_pose()) + self.assertEqual("ground_plane", light2.pose_relative_to()) + self.assertTrue(light2.cast_shadows()) + self.assertFalse(light2.light_on()) + self.assertFalse(light2.visualize()) + self.assertEqual(Color(0.4, 0.5, 0.6, 1), light2.diffuse()) + self.assertEqual(Color(0.8, 0.9, 0.1, 1), light2.specular()) + self.assertAlmostEqual(3.2, light2.attenuation_range()) + self.assertAlmostEqual(0.1, light2.linear_attenuation_factor()) + self.assertAlmostEqual(0.5, light2.constant_attenuation_factor()) + self.assertAlmostEqual(0.01, light2.quadratic_attenuation_factor()) + self.assertEqual(Vector3d(0.1, 0.2, 1), light2.direction()) + self.assertEqual(Angle(1.9), light2.spot_inner_angle()) + self.assertEqual(Angle(3.3), light2.spot_outer_angle()) + self.assertAlmostEqual(0.9, light2.spot_falloff()) + self.assertAlmostEqual(1.7, light2.intensity()) + + def test_spot_light_negative_values(self): + light = Light() + light.set_spot_falloff(-1.0) + self.assertAlmostEqual(0.0, light.spot_falloff()) + + light.set_spot_inner_angle(Angle(-1.0)) + self.assertAlmostEqual(0.0, light.spot_inner_angle().radian()) + + light.set_spot_outer_angle(Angle(-2.0)) + self.assertAlmostEqual(0.0, light.spot_outer_angle().radian()) + + def test_attenuation_clamp(self): + light = Light() + + light.set_linear_attenuation_factor(-1.0) + self.assertAlmostEqual(0.0, light.linear_attenuation_factor()) + + light.set_linear_attenuation_factor(20.0) + self.assertAlmostEqual(1.0, light.linear_attenuation_factor()) + + light.set_constant_attenuation_factor(-1.0) + self.assertAlmostEqual(0.0, light.constant_attenuation_factor()) + + light.set_constant_attenuation_factor(20.0) + self.assertAlmostEqual(1.0, light.constant_attenuation_factor()) + + light.set_quadratic_attenuation_factor(-1.0) + self.assertAlmostEqual(0.0, light.quadratic_attenuation_factor()) + + +if __name__ == '__main__': + unittest.main()