-
Notifications
You must be signed in to change notification settings - Fork 94
Getting Started 6: Creating a Renderer
If you have any cool ideas you want to try out for a new effect you will most likely end up writing a renderer with a different vertex format, shader, and custom update behavior. KivEnt tries to make this process as simple and pain-free as possible while keeping the rendering performant as it tends to eat up a huge amount of your frame time. In this tutorial we will write our own renderer to achieve this effect:
A user in #kivy wanted to animate between several starfields, I suggested that he just dynamically create the scene and he was a bit sceptical of Kivy being capable of handling enough objects to create a convincing star field. So let's do it.
In this tutorial, we will begin with the code from Getting Started 1.
The content of this tutorial covers the directories:
examples/7_star_fade
Prerequisites: You will need to have compiled kivent_core in addition to having kivy and all its requirements installed.
With Kivy's canvas instructions directly we would do something like:
self.instructions = []
with self.canvas:
for x in range(number_to_make):
col = Color((1., 1., 1., alpha)
rect = Rectangle(size=(w, h), pos=(x, y), source='texture.png')
instructions.append((col, rect))
and then somewhere later:
for col, rect in self.instructions:
col.a = new_alpha
rect.pos = new_pos
However, when we are batching all our drawing into one call we can no longer do the Color as a separate instruction. Each Color instruction would apply to the entire batch, not just one entity inside of it. The alternative is to move this data into the vertex and upload it with the rest of our vertex data.
The default vertex format in KivEnt looks like this (all vertex formats can be found in kivent_core.rendering.vertex_formats.pxd:
ctypedef struct VertexFormat4F:
GLfloat[2] pos
GLfloat[2] uvs
Right now we receive the coordinate of the vertex, and the uv coordinate of the texture. We need to introduce a GLfloat[4] to store our colors. We should not call it 'color' because there is an existing Kivy uniform named this we expect most shaders to have.
So our new format:
ctypedef struct VertexFormat8F:
GLfloat[2] pos
GLfloat[2] uvs
GLfloat[4] vColor
Now, in the vertex_formats.pyx we need to write a little code to allow us to interpret our vertex format from python. This is similar to how Kivy declares a vertex format, but contains an extra member in the (name, count, type) tuple that describes the starting position in bytes of the attribute (in C what would be the result of offsetof macro). We do not have access to the offsetof macro in cython, but we do have a slightly hack alternative. It looks like this:
from cython cimport Py_ssize_t
cdef extern from "Python.h":
ctypedef int Py_intptr_t
cdef VertexFormat4F* tmp1 = <VertexFormat4F*>NULL
pos_offset = <Py_ssize_t> (<Py_intptr_t>(tmp1.pos) - <Py_intptr_t>(tmp1))
uvs_offset = <Py_ssize_t> (<Py_intptr_t>(tmp1.uvs) - <Py_intptr_t>(tmp1))
vertex_format_4f = [
(b'pos', 2, b'float', pos_offset),
(b'uvs', 2, b'float', uvs_offset),
]
Let's make one for our new vertex format:
cdef VertexFormat8F* tmp3 = <VertexFormat8F*>NULL
pos_offset = <Py_ssize_t> (<Py_intptr_t>(tmp3.pos) - <Py_intptr_t>(tmp3))
uvs_offset = <Py_ssize_t> (<Py_intptr_t>(tmp3.uvs) - <Py_intptr_t>(tmp3))
color_offset = <Py_ssize_t> (<Py_intptr_t>(tmp3.vColor) - <Py_intptr_t>(tmp3))
vertex_format_8f = [
(b'pos', 2, b'float', pos_offset),
(b'uvs', 2, b'float', uvs_offset),
(b'vColor', 4, b'float', color_offset),
]
We need to b' the strings for Py2/3 compatibility as these should be bytestrings, not unicode since we will be sending them to GL. The format is (name, count, type, offset in struct). So for instance to represent r, g, b, a data we add a 4 float array.
This section will assume basic knowledge of the structure of a glsl shader. If you do not know about them read here.
The default shader for KivEnt looks like this:
---VERTEX SHADER---
#ifdef GL_ES
precision highp float;
#endif
/* Outputs to the fragment shader */
varying vec4 frag_color;
varying vec2 tex_coord0;
/* vertex attributes */
attribute vec2 pos;
attribute vec2 uvs;
/* uniform variables */
uniform mat4 modelview_mat;
uniform mat4 projection_mat;
uniform vec4 color;
uniform float opacity;
void main (void) {
frag_color = color * vec4(1.0, 1., 1.0, opacity);
tex_coord0 = uvs;
vec4 new_pos = vec4(pos.xy, 0.0, 1.0);
gl_Position = projection_mat * modelview_mat * new_pos;
}
---FRAGMENT SHADER---
#ifdef GL_ES
precision highp float;
#endif
/* Outputs from the vertex shader */
varying vec4 frag_color;
varying vec2 tex_coord0;
/* uniform texture samplers */
uniform sampler2D texture0;
void main (void){
gl_FragColor = frag_color * texture2D(texture0, tex_coord0);
}
We are only going to modify the vertex shader today, the fragment shader will stay the same.
First, let's introduce the new vColor attribute:
/* vertex attributes */
attribute vec2 pos;
attribute vec2 uvs;
attribute vec4 vColor;
Now, we modify the main so that frag_color takes into account our vColor:
void main (void) {
frag_color = vColor * color * vec4(1.0, 1., 1.0, opacity);
tex_coord0 = uvs;
vec4 new_pos = vec4(pos.xy, 0.0, 1.0);
gl_Position = projection_mat * modelview_mat * new_pos;
}
New shader:
---VERTEX SHADER---
#ifdef GL_ES
precision highp float;
#endif
/* Outputs to the fragment shader */
varying vec4 frag_color;
varying vec2 tex_coord0;
/* vertex attributes */
attribute vec2 pos;
attribute vec2 uvs;
attribute vec4 vColor;
/* uniform variables */
uniform mat4 modelview_mat;
uniform mat4 projection_mat;
uniform vec4 color;
uniform float opacity;
void main (void) {
frag_color = vColor * color * vec4(1.0, 1., 1.0, opacity);
tex_coord0 = uvs;
vec4 new_pos = vec4(pos.xy, 0.0, 1.0);
gl_Position = projection_mat * modelview_mat * new_pos;
}
---FRAGMENT SHADER---
#ifdef GL_ES
precision highp float;
#endif
/* Outputs from the vertex shader */
varying vec4 frag_color;
varying vec2 tex_coord0;
/* uniform texture samplers */
uniform sampler2D texture0;
void main (void){
gl_FragColor = frag_color * texture2D(texture0, tex_coord0);
}
The basic Renderer's update looks like this, I have added comments to explain the algorithm:
def update(self, dt):
cdef IndexedBatch batch
cdef list batches
cdef unsigned int batch_key
cdef unsigned int index_offset, vert_offset
cdef RenderStruct* render_comp
cdef PositionStruct2D* pos_comp
cdef VertexFormat4F* frame_data
cdef GLushort* frame_indices
cdef VertMesh vert_mesh
cdef float* mesh_data
cdef VertexFormat4F* vertex
cdef unsigned short* mesh_indices
cdef unsigned int used, i, real_index, component_count
cdef ComponentPointerAggregator entity_components
cdef int attribute_count = self.attribute_count
cdef BatchManager batch_manager = self.batch_manager
cdef dict batch_groups = batch_manager.batch_groups
cdef list meshes = model_manager.meshes
cdef CMesh mesh_instruction
cdef MemoryBlock components_block
cdef void** component_data
##Go through every batch_key, each batch_key represents
##the rendering for a single texture file.
for batch_key in batch_groups:
batches = batch_groups[batch_key]
#Go through each batch in the batch_group
for batch in batches:
#Retrieve the ComponentPointerAggregator for this batch.
entity_components = batch.entity_components
#Retrieve the memory for this block
components_block = entity_components.memory_block
#Find out how many entities we used
used = components_block.used_count
#Find out how many components per entity.
component_count = entity_components.count
#Cast the data to its type in this case void** (an array of void*)
component_data = <void**>components_block.data
#Get the appropriate buffer to fill with vertex data
frame_data = <VertexFormat4F*>batch.get_vbo_frame_to_draw()
#Get the appropriate buffer to fill with indices data
frame_indices = <GLushort*>batch.get_indices_frame_to_draw()
#Keep track of the number of indices we have drawn this frame.
index_offset = 0
#Now for each entry in the entity components (0 to used)
for i in range(used):
#Calculate the actual index, there are i sets of component_count
#pointers representing the components for each entity
real_index = i * component_count
#Check that the pointer is not NULL (would mean this component
#is inactive.
if component_data[real_index] == NULL:
continue
#Get the pointer to our system component and cast it to
#its actual type. Each component will be at the index
#of its sytem_id in the system_names ListProperty.
render_comp = <RenderStruct*>component_data[real_index+0]
#Get the offset of this entity in the vertex buffer.
vert_offset = render_comp.vert_index
#Get the actual model
vert_mesh = meshes[render_comp.vert_index_key]
#The number of vertices in the model
vertex_count = vert_mesh._vert_count
#The number of indices
index_count = vert_mesh._index_count
#Check if we should actual render:
if render_comp.render:
#Get the position component.
pos_comp = <PositionStruct2D*>component_data[
real_index+1]
#Get the data for the meshes vertices
mesh_data = vert_mesh._data
#and indices
mesh_indices = vert_mesh._indices
#Starting at index_offset, write the index at
#position i into index_offset + i of frame
#indices
for i in range(index_count):
frame_indices[i+index_offset] = (
mesh_indices[i] + vert_offset)
#For each vertex in the model
for n in range(vertex_count):
#Get its position in the frame data
vertex = &frame_data[n + vert_offset]
#update the position attribute with the
#position of the entity and the position
#of that vertex in the model.
vertex.pos[0] = pos_comp.x + mesh_data[
n*attribute_count]
vertex.pos[1] = pos_comp.y + mesh_data[
n*attribute_count+1]
#Grab the uvs for the vertex from the mesh
vertex.uvs[0] = mesh_data[n*attribute_count+2]
vertex.uvs[1] = mesh_data[n*attribute_count+3]
#Increment the index_offset by the number of indices
#used for this component.
index_offset += index_count
#Tell the batch how many indices to draw next frame.
batch.set_index_count_for_frame(index_offset)
##Get the actual canvas instruction and flag it for update.
mesh_instruction = batch.mesh_instruction
mesh_instruction.flag_update()
For the most part everything before the vertex_count loop is going to stay the same, there are a couple static declarations we need to change the type of for our new VertexFormat8F and we need to introduce the color component.
First, we declare a new class and modify some of the defaults for Renderer:
cdef class ColorRenderer(Renderer):
##We need to tell the default names of the components
##that are retrieved when processing.
system_names = ListProperty(['color_renderer', 'position',
'color'])
#set the default name for this GameSystem
system_id = StringProperty('color_renderer')
#Tell it how big our VertexFormat struct is in bytes.
vertex_format_size = NumericProperty(sizeof(VertexFormat8F))
Next we need to modify the initialization to take into account our new type (the VertexFormat8F struct) and the associated vertex_format_8f python readable spec for the format.
#This function is called as part of initialization, we need to tell it
#Which Vertex Format to use. Everything else should stay the same as the
#other renderers
cdef void* setup_batch_manager(self, Buffer master_buffer) except NULL:
cdef KEVertexFormat batch_vertex_format = KEVertexFormat(
sizeof(VertexFormat8F), *vertex_format_8f)
self.batch_manager = BatchManager(
self.size_of_batches, self.max_batches, self.frame_count,
batch_vertex_format, master_buffer, 'triangles', self.canvas,
[x for x in self.system_names],
self.smallest_vertex_count, self.gameworld)
return <void*>self.batch_manager
Finally, we need to modify the update function:
def update(self, dt):
cdef IndexedBatch batch
cdef list batches
cdef unsigned int batch_key
cdef unsigned int index_offset, vert_offset
cdef RenderStruct* render_comp
cdef PositionStruct2D* pos_comp
#declare color_comp's type
cdef ColorStruct* color_comp
#Change the type of frame_data
cdef VertexFormat8F* frame_data
cdef GLushort* frame_indices
cdef VertMesh vert_mesh
cdef float* mesh_data
#Change the type of the vertex
cdef VertexFormat8F* vertex
cdef unsigned short* mesh_indices
cdef unsigned int used, i, real_index, component_count, x, y
cdef ComponentPointerAggregator entity_components
cdef int attribute_count = self.attribute_count
cdef BatchManager batch_manager = self.batch_manager
cdef dict batch_groups = batch_manager.batch_groups
cdef list meshes = model_manager.meshes
cdef CMesh mesh_instruction
cdef MemoryBlock components_block
cdef void** component_data
for batch_key in batch_groups:
batches = batch_groups[batch_key]
for batch in batches:
entity_components = batch.entity_components
components_block = entity_components.memory_block
used = components_block.used_count
component_count = entity_components.count
component_data = <void**>components_block.data
frame_data = <VertexFormat8F*>batch.get_vbo_frame_to_draw()
frame_indices = <GLushort*>batch.get_indices_frame_to_draw()
index_offset = 0
for i in range(components_block.size):
real_index = i * component_count
if component_data[real_index] == NULL:
continue
render_comp = <RenderStruct*>component_data[real_index+0]
vert_offset = render_comp.vert_index
vert_mesh = meshes[render_comp.vert_index_key]
vertex_count = vert_mesh._vert_count
index_count = vert_mesh._index_count
if render_comp.render:
pos_comp = <PositionStruct2D*>component_data[
real_index+1]
mesh_data = vert_mesh._data
#look up our ColorStruct too
color_comp = <ColorStruct*>component_data[real_index+2]
mesh_indices = vert_mesh._indices
for y in range(index_count):
frame_indices[y+index_offset] = (
mesh_indices[y] + vert_offset)
for n in range(vertex_count):
vertex = &frame_data[n + vert_offset]
vertex.pos[0] = pos_comp.x + (
mesh_data[n*attribute_count])
vertex.pos[1] = pos_comp.y + (
mesh_data[n*attribute_count+1])
vertex.uvs[0] = mesh_data[n*attribute_count+2]
vertex.uvs[1] = mesh_data[n*attribute_count+3]
#modify the vColor prop using the color_comp
vertex.vColor[0] = color_comp.r
vertex.vColor[1] = color_comp.g
vertex.vColor[2] = color_comp.b
vertex.vColor[3] = color_comp.a
index_offset += index_count
batch.set_index_count_for_frame(index_offset)
mesh_instruction = batch.mesh_instruction
mesh_instruction.flag_update()
This ColorRenderer has been included in kivent_core so you ought to be able to import it from systems.renderers.
The only part left is to build a GameSystem that lerps the alpha value of the ColorComponent from 0.0 to 1.0 to fade in and back from 1.0 to 0.0 to fade out.
Lerp is a linear interpolation that smoothly moves from one value to another across a given period of time. Our implementation will look like, where t is given in the interval: [0, 1.0]:
def lerp(v0, v1, t):
return (1-t)*v0 + t * v1
Our new GameSystem is going to have 3 attributes:
- time: The total amount of time for fade in + fade out
- current_time: The amount of time that has been used, we will start at 0.
- fade_out_start: The time when the fade out will begin.
In addition, when one entity is done fading out, we will remove it and create a new entity.
Here is our FadingSystem:
class FadingSystem(GameSystem):
#We create an ObjectProperty so we can bind the create_entity
#Function we want to use when the old object dies.
make_entity = ObjectProperty(None)
def update(self, dt):
#Get the entities
entities = self.gameworld.entities
#Check each component
for component in self.components:
if component is not None:
#Look up entity_id for component if its not None
entity_id = component.entity_id
#Get the entity
entity = entities[entity_id]
#Get the color comp
color_comp = entity.color
#add frame time to the current_time.
component.current_time += dt
current_time = component.current_time
fade_out_start = component.fade_out_start
time = component.time
#calculate length of fade_out
fade_out_time = time - fade_out_start
#If we are past lifetime, remove and make a new
#entity.
if current_time >= time:
self.gameworld.remove_entity(entity_id)
self.make_entity()
#If we are still before fade out should begin, lerp from 0. to 1.
if current_time < fade_out_start:
color_comp.a = lerp(0., 1., current_time / fade_out_start)
else:
#Otherwise lerp from 1.0 to 0.
color_comp.a = lerp(1., 0.,
(current_time - fade_out_start) / fade_out_time)
We have 6 graphics found in examples/7_star_fade/assets, however we are going to load in far more than 6 models using the various textures at different sizes:
#load the atlas
texture_manager.load_atlas('assets/stars.atlas')
#texkeys to choose from
keys = ['star1', 'star2', 'star3', 'star_circle', 'star_square']
#where we will store the names of models created
model_keys = []
mk_a = model_keys.append
#This function takes arguments attributes per vertex, width, height,
#texture key, name of the model
load_textured_rectangle = model_manager.load_textured_rectangle
for x in range(250):
#generate a unique str name for the model
model_key = 'star_m_' + str(x)
#choose a texkey
tex_key = choice(keys)
#we will use the same height and width as our tex is square
wh = randrange(1., 7.)
#invoke function
load_textured_rectangle(4, wh, wh, choice(keys), model_key)
#add the texkey and modelkey to our model_keys list for later
mk_a((model_key, tex_key))
Let's modify the TestGame class to support our new FadingSystem. Change the init to include it:
def __init__(self, **kwargs):
super(TestGame, self).__init__(**kwargs)
self.gameworld.init_gameworld(
['color', 'position', 'renderer', 'fade'],
callback=self.init_game)
We are going to split entity creation out into a function that does it one at a time so that we can call it when an entity dies (bind it to the make_entity property of FadingSystem).
def draw_some_stuff(self):
init_entity = self.gameworld.init_entity
for x in range(5000):
self.draw_a_star()
def draw_a_star(self):
model_to_use = choice(model_keys)
pos = randint(0, Window.width), randint(0, Window.height)
fade_in = randrange(10., 15.)
fade_out = randrange(10., 15.)
create_dict = {
'position': pos,
'color': (1., 1., 1., 0.),
'renderer': {'texture': model_to_use[1],
'vert_mesh_key': model_to_use[0]},
'fade': {'time': fade_in + fade_out,
'fade_out_start': fade_in,
'current_time': 0,},
}
ent = self.gameworld.init_entity(create_dict, ['position', 'color',
'renderer', 'fade'])
Note when making a dynamic python GameSystem we only need to declare the attributes by name in a dict during creation in order to initialize them, unlike the StaticMemGameSystem that must create a struct that hold its data.
Let's also modify the 'main' state to include our new GameSystem as it is updateable.
def setup_states(self):
self.gameworld.add_state(state_name='main',
systems_added=['renderer'],
systems_removed=[], systems_paused=[],
systems_unpaused=['renderer', 'fade'],
screenmanager_screen='main')
Finally, add FadingSystem to the kv file:
GameWorld:
id: gameworld
gamescreenmanager: gamescreenmanager
size_of_gameworld: 100*1024
size_of_entity_block: 128
system_count: 4
size: root.size
pos: root.pos
zones: {'general': 10000}
PositionSystem2D:
system_id: 'position'
gameworld: gameworld
zones: ['general']
size_of_component_block: 128
ColorSystem:
system_id: 'color'
gameworld: gameworld
zones: ['general']
ColorRenderer:
gameworld: gameworld
system_id: 'renderer'
zones: ['general']
frame_count: 3
updateable: True
system_names: ['renderer', 'position','color']
size_of_batches: 256
size_of_component_block: 128
shader_source: 'assets/glsl/positioncolor.glsl'
FadingSystem:
gameworld: gameworld
system_id: 'fade'
updateable: True
make_entity: root.draw_a_star
Note that if we change the system_id of ColorRenderer, we must also change the system_names list to use that system_id. Don't forget to bind our create entity function (draw_a_star) to make_entity property of FadingSystem and set updateable to True.
python:
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock
from kivy.core.window import Window
from random import randint, choice, randrange
import kivent_core
from kivent_core.gameworld import GameWorld
from kivent_core.systems.position_systems import PositionSystem2D
from kivent_core.systems.renderers import Renderer
from kivent_core.systems.gamesystem import GameSystem
from kivent_core.managers.resource_managers import texture_manager
from kivy.properties import StringProperty, ObjectProperty
from kivy.factory import Factory
from kivent_core.managers.resource_managers import texture_manager
from os.path import dirname, join, abspath
texture_manager.load_atlas('assets/stars.atlas')
def lerp(v0, v1, t):
return (1.-t)*v0 + t * v1
class FadingSystem(GameSystem):
make_entity = ObjectProperty(None)
def update(self, dt):
entities = self.gameworld.entities
for component in self.components:
if component is not None:
entity_id = component.entity_id
entity = entities[entity_id]
color_comp = entity.color
component.current_time += dt
current_time = component.current_time
fade_out_start = component.fade_out_start
time = component.time
fade_out_time = time - fade_out_start
if current_time >= time:
self.gameworld.remove_entity(entity_id)
self.make_entity()
if current_time < fade_out_start:
color_comp.a = lerp(0., 255.,
current_time / fade_out_start)
else:
color_comp.a = lerp(255., 0.,
(current_time - fade_out_start) / fade_out_time)
Factory.register('FadingSystem', cls=FadingSystem)
class TestGame(Widget):
def __init__(self, **kwargs):
super(TestGame, self).__init__(**kwargs)
self.gameworld.init_gameworld(
['color', 'position', 'renderer', 'fade'],
callback=self.init_game)
def init_game(self):
self.setup_states()
self.load_resources()
self.set_state()
self.draw_some_stuff()
def load_resources(self):
keys = ['star1', 'star2', 'star3', 'star_circle', 'star_square']
self.model_keys = model_keys = []
mk_a = model_keys.append
model_manager = self.gameworld.model_manager
load_textured_rectangle = model_manager.load_textured_rectangle
for x in range(100):
model_key = 'star_m_' + str(x)
tex_key = choice(keys)
wh = randrange(1., 7.)
real_name = load_textured_rectangle(
'vertex_format_4f', wh, wh, tex_key, model_key)
mk_a((model_key, tex_key))
def draw_some_stuff(self):
init_entity = self.gameworld.init_entity
for x in range(1000):
self.draw_a_star()
def draw_a_star(self):
model_to_use = choice(self.model_keys)
pos = randint(0, Window.width), randint(0, Window.height)
fade_in = randrange(10., 15.)
fade_out = randrange(10., 15.)
create_dict = {
'position': pos,
'color': (255, 255, 255, 0),
'renderer': {'texture': model_to_use[1],
'model_key': model_to_use[0]},
'fade': {'time': fade_in + fade_out,
'fade_out_start': fade_in,
'current_time': 0,},
}
ent = self.gameworld.init_entity(create_dict, ['position', 'color',
'renderer', 'fade'])
def setup_states(self):
self.gameworld.add_state(state_name='main',
systems_added=['renderer'],
systems_removed=[], systems_paused=[],
systems_unpaused=['renderer', 'fade'],
screenmanager_screen='main')
def set_state(self):
self.gameworld.state = 'main'
class DebugPanel(Widget):
fps = StringProperty(None)
def __init__(self, **kwargs):
super(DebugPanel, self).__init__(**kwargs)
Clock.schedule_once(self.update_fps)
def update_fps(self,dt):
self.fps = str(int(Clock.get_fps()))
Clock.schedule_once(self.update_fps, .05)
class YourAppNameApp(App):
def build(self):
Window.clearcolor = (0, 0, 0, 1.)
if __name__ == '__main__':
YourAppNameApp().run()
kv:
#:kivy 1.9.0
TestGame:
<TestGame>:
gameworld: gameworld
GameWorld:
id: gameworld
gamescreenmanager: gamescreenmanager
size_of_gameworld: 100*1024
size_of_entity_block: 128
system_count: 4
size: root.size
pos: root.pos
zones: {'general': 10000}
PositionSystem2D:
system_id: 'position'
gameworld: gameworld
zones: ['general']
size_of_component_block: 128
ColorSystem:
system_id: 'color'
gameworld: gameworld
zones: ['general']
ColorRenderer:
gameworld: gameworld
system_id: 'renderer'
zones: ['general']
frame_count: 3
updateable: True
system_names: ['renderer', 'position','color']
size_of_batches: 256
size_of_component_block: 128
shader_source: 'assets/glsl/positioncolor.glsl'
FadingSystem:
gameworld: gameworld
system_id: 'fade'
updateable: True
make_entity: root.draw_a_star
GameScreenManager:
id: gamescreenmanager
size: root.size
pos: root.pos
gameworld: gameworld
<GameScreenManager>:
MainScreen:
id: main_screen
<MainScreen@GameScreen>:
name: 'main'
FloatLayout:
DebugPanel:
size_hint: (.2, .1)
pos_hint: {'x': .225, 'y': .025}
<DebugPanel>:
Label:
pos: root.pos
size: root.size
font_size: root.size[1]*.5
halign: 'center'
valign: 'middle'
color: (1,1,1,1)
text: 'FPS: ' + root.fps if root.fps != None else 'FPS:'