From 06af8eeddd5f4c7f9566a63ab7a33c90fb77df01 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Fri, 29 Dec 2017 13:01:40 +0100 Subject: [PATCH] Stencil: handle stencil state in a way it can be saved/restored. Now the FBO can save/reset/restore the stencil state. This fixes many issues related to stencil not working withing FBO (FBO with StencilView/Graph inside ScreenManager, Carousel, etc). Closes #1578 Closes #4454 Ref #3674 Closes kivy-garden/garden.graph#7 --- kivy/graphics/fbo.pxd | 1 + kivy/graphics/fbo.pyx | 9 ++ kivy/graphics/stencil_instructions.pxd | 4 + kivy/graphics/stencil_instructions.pyx | 142 +++++++++++++++++-------- kivy/uix/effectwidget.py | 1 + kivy/uix/screenmanager.py | 2 +- 6 files changed, 112 insertions(+), 47 deletions(-) diff --git a/kivy/graphics/fbo.pxd b/kivy/graphics/fbo.pxd index 59f2f8933..43393d399 100644 --- a/kivy/graphics/fbo.pxd +++ b/kivy/graphics/fbo.pxd @@ -15,6 +15,7 @@ cdef class Fbo(RenderContext): cdef GLint _viewport[4] cdef Texture _texture cdef int _is_bound + cdef object _stencil_state cdef list observers cpdef clear_buffer(self) diff --git a/kivy/graphics/fbo.pyx b/kivy/graphics/fbo.pyx index 2dc9c2346..ee4736471 100644 --- a/kivy/graphics/fbo.pyx +++ b/kivy/graphics/fbo.pyx @@ -82,6 +82,8 @@ from kivy.graphics.cgl cimport * from kivy.graphics.instructions cimport RenderContext, Canvas from kivy.graphics.opengl import glReadPixels as py_glReadPixels +from kivy.graphics.stencil_instructions cimport ( + get_stencil_state, restore_stencil_state, reset_stencil_state) cdef list fbo_stack = [] cdef list fbo_release_list = [] @@ -312,6 +314,10 @@ cdef class Fbo(RenderContext): cgl.glGetIntegerv(GL_VIEWPORT, self._viewport) cgl.glViewport(0, 0, self._width, self._height) + # save stencil stack + self._stencil_state = get_stencil_state() + reset_stencil_state() + cpdef release(self): '''Release the Framebuffer (unbind). ''' @@ -329,6 +335,9 @@ cdef class Fbo(RenderContext): cgl.glViewport(self._viewport[0], self._viewport[1], self._viewport[2], self._viewport[3]) + # restore stencil stack + restore_stencil_state(self._stencil_state) + cpdef clear_buffer(self): '''Clear the framebuffer with the :attr:`clear_color`. diff --git a/kivy/graphics/stencil_instructions.pxd b/kivy/graphics/stencil_instructions.pxd index 4d33c4476..7f175387b 100644 --- a/kivy/graphics/stencil_instructions.pxd +++ b/kivy/graphics/stencil_instructions.pxd @@ -1,5 +1,9 @@ from kivy.graphics.instructions cimport Instruction +cdef get_stencil_state() +cdef void restore_stencil_state(dict state) +cdef void reset_stencil_state() + cdef class StencilPush(Instruction): cdef int apply(self) except -1 cdef class StencilPop(Instruction): diff --git a/kivy/graphics/stencil_instructions.pyx b/kivy/graphics/stencil_instructions.pyx index 609fd5e9c..089918616 100644 --- a/kivy/graphics/stencil_instructions.pyx +++ b/kivy/graphics/stencil_instructions.pyx @@ -103,9 +103,13 @@ from kivy.graphics.cgl cimport * from kivy.compat import PY2 from kivy.graphics.instructions cimport Instruction -cdef int _stencil_level = 0 -cdef int _stencil_in_push = 0 +cdef dict DEFAULT_STATE = { + "level": 0, + "in_push": False, + "op": None, + "gl_stencil_func": None} +cdef dict _stencil_state = DEFAULT_STATE.copy() cdef dict _gl_stencil_op = { 'never': GL_NEVER, 'less': GL_LESS, 'equal': GL_EQUAL, @@ -123,26 +127,42 @@ cdef inline int _stencil_op_to_gl(x): raise Exception('Unknown <%s> stencil op' % x) -cdef class StencilPush(Instruction): - '''Push the stencil stack. See the module documentation for more - information. - ''' - cdef int apply(self) except -1: - global _stencil_level, _stencil_in_push - if _stencil_in_push: - raise Exception('Cannot use StencilPush inside another ' - 'StencilPush.\nUse StencilUse before.') - _stencil_in_push = 1 - _stencil_level += 1 +cdef get_stencil_state(): + global _stencil_state + return _stencil_state.copy() - if _stencil_level == 1: + +cdef void restore_stencil_state(dict state): + global _stencil_state + _stencil_state = state.copy() + stencil_apply_state(_stencil_state, True) + + +cdef void reset_stencil_state(): + restore_stencil_state(DEFAULT_STATE) + + +cdef void stencil_apply_state(dict state, restore_only): + # apply state for stencil here. This allow to reapply a state + # easily when using FBO, or linking to other GL subprogram + if state["op"] is None: + cgl.glDisable(GL_STENCIL_TEST) + + elif state["op"] == "push": + # Push the stencil stack, ready to draw a mask + + if not restore_only: + state["level"] += 1 + state["in_push"] = True + + if state["level"] == 1: cgl.glStencilMask(0xff) log_gl_error('StencilPush.apply-glStencilMask') cgl.glClearStencil(0) log_gl_error('StencilPush.apply-glClearStencil') cgl.glClear(GL_STENCIL_BUFFER_BIT) log_gl_error('StencilPush.apply-glClear(GL_STENCIL_BUFFER_BIT)') - if _stencil_level > 128: + elif state["level"] > 128: raise Exception('Cannot push more than 128 level of stencil.' ' (stack overflow)') @@ -154,28 +174,68 @@ cdef class StencilPush(Instruction): log_gl_error('StencilPush.apply-glStencilOp') cgl.glColorMask(False, False, False, False) log_gl_error('StencilPush.apply-glColorMask') + + + elif state["op"] == "pop": + # Pop the stencil stack + + if not restore_only: + if state["level"] == 0: + raise Exception('Too much StencilPop (stack underflow)') + state["level"] -= 1 + state["in_push"] = False + + cgl.glColorMask(True, True, True, True) + log_gl_error('StencilPop.apply-glColorMask') + if state["level"] == 0: + cgl.glDisable(GL_STENCIL_TEST) + log_gl_error('StencilPop.apply-glDisable') + return + # reset for previous + cgl.glStencilFunc(GL_EQUAL, state["level"], 0xff) + log_gl_error('StencilPop.apply-glStencilFunc') + cgl.glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) + log_gl_error('StencilPop.apply-glStencilOp') + + + elif state["op"] == "use": + # Use the current stencil buffer to cut the drawing + if not restore_only: + state["in_push"] = False + cgl.glColorMask(True, True, True, True) + log_gl_error('StencilUse.apply-glColorMask') + cgl.glStencilFunc(state["gl_stencil_func"], state["level"], 0xff) + log_gl_error('StencilUse.apply-glStencilFunc') + cgl.glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) + cgl.glEnable(GL_STENCIL_TEST) + log_gl_error('StencilUse.apply-glStencilOp') + + elif state["op"] == "unuse": + # Ready to undraw the mask + cgl.glStencilFunc(GL_GREATER, 0xff, 0xff) + log_gl_error('StencilUnUse.apply-glStencilFunc') + cgl.glStencilOp(GL_DECR, GL_DECR, GL_DECR) + log_gl_error('StencilUnUse.apply-glStencilOp') + cgl.glColorMask(False, False, False, False) + log_gl_error('StencilUnUse.apply-glColorMask') + + +cdef class StencilPush(Instruction): + '''Push the stencil stack. See the module documentation for more + information. + ''' + cdef int apply(self) except -1: + _stencil_state["op"] = "push" + stencil_apply_state(_stencil_state, False) return 0 + cdef class StencilPop(Instruction): '''Pop the stencil stack. See the module documentation for more information. ''' cdef int apply(self) except -1: - global _stencil_level, _stencil_in_push - if _stencil_level == 0: - raise Exception('Too much StencilPop (stack underflow)') - _stencil_level -= 1 - _stencil_in_push = 0 - cgl.glColorMask(True, True, True, True) - log_gl_error('StencilPop.apply-glColorMask') - if _stencil_level == 0: - cgl.glDisable(GL_STENCIL_TEST) - log_gl_error('StencilPop.apply-glDisable') - return 0 - # reset for previous - cgl.glStencilFunc(GL_EQUAL, _stencil_level, 0xff) - log_gl_error('StencilPop.apply-glStencilFunc') - cgl.glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) - log_gl_error('StencilPop.apply-glStencilOp') + _stencil_state["op"] = "pop" + stencil_apply_state(_stencil_state, False) return 0 @@ -191,15 +251,9 @@ cdef class StencilUse(Instruction): self._op = GL_EQUAL cdef int apply(self) except -1: - global _stencil_in_push - _stencil_in_push = 0 - cgl.glColorMask(True, True, True, True) - log_gl_error('StencilUse.apply-glColorMask') - cgl.glStencilFunc(self._op, _stencil_level, 0xff) - log_gl_error('StencilUse.apply-glStencilFunc') - cgl.glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP) - cgl.glEnable(GL_STENCIL_TEST) - log_gl_error('StencilUse.apply-glStencilOp') + _stencil_state["gl_stencil_func"] = self._op + _stencil_state["op"] = "use" + stencil_apply_state(_stencil_state, False) return 0 property func_op: @@ -230,10 +284,6 @@ cdef class StencilUnUse(Instruction): '''Use current stencil buffer to unset the mask. ''' cdef int apply(self) except -1: - cgl.glStencilFunc(GL_GREATER, 0xff, 0xff) - log_gl_error('StencilUnUse.apply-glStencilFunc') - cgl.glStencilOp(GL_DECR, GL_DECR, GL_DECR) - log_gl_error('StencilUnUse.apply-glStencilOp') - cgl.glColorMask(False, False, False, False) - log_gl_error('StencilUnUse.apply-glColorMask') + _stencil_state["op"] = "unuse" + stencil_apply_state(_stencil_state, False) return 0 diff --git a/kivy/uix/effectwidget.py b/kivy/uix/effectwidget.py index 66e233875..9c00549b1 100644 --- a/kivy/uix/effectwidget.py +++ b/kivy/uix/effectwidget.py @@ -585,6 +585,7 @@ class EffectFbo(Fbo): attempts to set a new shader. See :meth:`set_fs`. ''' def __init__(self, *args, **kwargs): + kwargs.setdefaults("with_stencilbuffer", True) super(EffectFbo, self).__init__(*args, **kwargs) self.texture_rectangle = None diff --git a/kivy/uix/screenmanager.py b/kivy/uix/screenmanager.py index 2794c81b9..22224d177 100644 --- a/kivy/uix/screenmanager.py +++ b/kivy/uix/screenmanager.py @@ -469,7 +469,7 @@ class ShaderTransition(TransitionBase): and defaults to [0, 0, 0, 1].''' def make_screen_fbo(self, screen): - fbo = Fbo(size=screen.size) + fbo = Fbo(size=screen.size, with_stencilbuffer=True) with fbo: ClearColor(*self.clearcolor) ClearBuffers()