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
This commit is contained in:
Mathieu Virbel 2017-12-29 13:01:40 +01:00
parent 4c6f7be666
commit 06af8eeddd
6 changed files with 112 additions and 47 deletions

View File

@ -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)

View File

@ -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, <GLint *>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`.

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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()