diff --git a/arcade/gl/context.py b/arcade/gl/context.py index c804ad2ae..25b197af2 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -211,6 +211,17 @@ def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl self._point_size = 1.0 self._flags: Set[int] = set() self._wireframe = False + # Options for cull_face + self._cull_face_options = { + "front": gl.GL_FRONT, + "back": gl.GL_BACK, + "front_and_back": gl.GL_FRONT_AND_BACK, + } + self._cull_face_options_reverse = { + gl.GL_FRONT: "front", + gl.GL_BACK: "back", + gl.GL_FRONT_AND_BACK: "front_and_back", + } # Context GC as default. We need to call Context.gc() to free opengl resources self._gc_mode = "context_gc" @@ -618,8 +629,50 @@ def blend_func(self, value: Union[Tuple[int, int], Tuple[int, int, int, int]]): ValueError("blend_func takes a tuple of 2 or 4 values") # def blend_equation(self) - # def front_face(self) - # def cull_face(self) + + @property + def front_face(self) -> str: + """ + Configure front face winding order of triangles. + + By default the counter-clockwise winding side is the front face. + This can be set set to clockwise or counter-clockwise:: + + ctx.front_face = "cw" + ctx.front_face = "ccw" + """ + value = c_int() + gl.glGetIntegerv(gl.GL_FRONT_FACE, value) + return "cw" if value.value == gl.GL_CW else "ccw" + + @front_face.setter + def front_face(self, value: str): + if value not in ["cw", "ccw"]: + raise ValueError("front_face must be 'cw' or 'ccw'") + gl.glFrontFace(gl.GL_CW if value == "cw" else gl.GL_CCW) + + @property + def cull_face(self) -> str: + """ + The face side to cull when face culling is enabled. + + By default the back face is culled. This can be set to + front, back or front_and_back:: + + ctx.cull_face = "front" + ctx.cull_face = "back" + ctx.cull_face = "front_and_back" + """ + value = c_int() + gl.glGetIntegerv(gl.GL_CULL_FACE_MODE, value) + return self._cull_face_options_reverse[value.value] + + @cull_face.setter + def cull_face(self, value): + if value not in self._cull_face_options: + raise ValueError("cull_face must be", list(self._cull_face_options.keys())) + + gl.glCullFace(self._cull_face_options[value]) @property def wireframe(self) -> bool: diff --git a/doc/programming_guide/release_notes.rst b/doc/programming_guide/release_notes.rst index 1925d0759..7b4fa8749 100644 --- a/doc/programming_guide/release_notes.rst +++ b/doc/programming_guide/release_notes.rst @@ -187,6 +187,8 @@ Changes * Uniforms are now set using ``glProgramUniform`` instead of ``glUniform`` when the extension is available. * Fixed many implicit type conversions in the shader code for wider support. + * Added ``front_face`` property on the context for configuring front face winding order of triangles + * Added ``cull_face`` property on the context for configuring what triangle face to cull * :py:class:`~arcade.tilemap.TileMap` diff --git a/tests/unit/gl/test_opengl_context.py b/tests/unit/gl/test_opengl_context.py index f6d87d0bf..92069846a 100644 --- a/tests/unit/gl/test_opengl_context.py +++ b/tests/unit/gl/test_opengl_context.py @@ -149,3 +149,35 @@ def test_shader_include_fail(ctx): """ with pytest.raises(FileNotFoundError): ctx.shader_inc(src) + + +def test_front_face(ctx): + """Test front face""" + # Default + assert ctx.front_face == "ccw" + + # Set valid values + ctx.front_face = "cw" + assert ctx.front_face == "cw" + ctx.front_face = "ccw" + assert ctx.front_face == "ccw" + + # Set invalid value + with pytest.raises(ValueError): + ctx.front_face = "moo" + + +def test_cull_face(ctx): + assert ctx.cull_face == "back" + + # Set valid values + ctx.cull_face = "front" + assert ctx.cull_face == "front" + ctx.cull_face = "back" + assert ctx.cull_face == "back" + ctx.cull_face = "front_and_back" + assert ctx.cull_face == "front_and_back" + + # Set invalid value + with pytest.raises(ValueError): + ctx.cull_face = "moo"