Skip to content

Instantly share code, notes, and snippets.

@tshirtman
Last active September 2, 2024 10:12
Show Gist options
  • Save tshirtman/222abcb1e6808c89192934996740adb5 to your computer and use it in GitHub Desktop.
Save tshirtman/222abcb1e6808c89192934996740adb5 to your computer and use it in GitHub Desktop.
a tilemap implementation in kivy using a shader
from array import array
from kivy.uix.widget import Widget
from kivy.clock import Clock
from kivy import properties as P
from kivy.graphics import (
RenderContext, BindTexture, Rectangle, Color
)
from kivy.graphics.texture import Texture
from kivy.core.window import Window
SHILE_SET = """
$HEADER$
uniform sampler2D world_texture;
uniform float time;
uniform vec2 resolution;
uniform vec2 tile_size;
uniform vec2 offset;
uniform vec2 world_resolution;
uniform float zoom;
uniform vec2 source_resolution;
void main(void){
// pixel position in widget
vec4 frag_coord = frag_modelview_mat * gl_FragCoord;
vec2 frag_pos = vec2(frag_coord.xy - offset) / zoom;
// pixel position in world
vec2 pos = frag_pos / zoom;
// cell and offset from cell origin
vec2 cell_pos = frag_pos / tile_size;
vec2 pixel_coord = mod(frag_pos, tile_size);
vec2 tile_id = texture2D(world_texture, cell_pos / world_resolution).rg * 255.;
// lookup pixel at position of tile in tileset
vec2 pixel_pos = vec2(tile_id * tile_size + pixel_coord);
gl_FragColor = texture2D(texture0, pixel_pos / source_resolution);
}
"""
class ShileSet(Widget):
offset = P.ListProperty([0, 0])
source = P.StringProperty()
world = P.ListProperty([])
tile_size = P.ListProperty([8, 8])
zoom = P.NumericProperty(1)
fs = P.StringProperty(SHILE_SET)
def __init__(self, **kwargs):
self.canvas = RenderContext(fs=self.fs)
super().__init__(**kwargs)
Clock.schedule_interval(self.update_glsl, 0)
with self.canvas:
Color(1, 1, 1, 1, mode='rgba')
self.rectangle = Rectangle(
size=self.size,
pos=self.pos,
source=self.source,
)
self.bind(
size=self.update_rect,
pos=self.update_rect,
source=self.update_rect,
world=self.update_world
)
def update_rect(self, *args):
self.rectangle.pos = self.pos
self.rectangle.size = self.size
self.rectangle.source = self.source
def on_fs(self, instance, value):
shader = self.canvas.shader
old_value = shader.fs
shader.fs = value
if not shader.success:
shader.fs = old_value
raise Exception("failed")
def update_glsl(self, dt):
self.canvas["time"] = Clock.get_boottime()
self.canvas["resolution"] = [float(x) for x in self.size]
self.canvas["offset"] = [float(x) for x in self.offset]
self.canvas["zoom"] = float(self.zoom)
self.canvas["tile_size"] = [float(x) for x in self.tile_size]
self.canvas["world_texture"] = 1
self.canvas["source_resolution"] = [float(x) for x in self.source_resolution]
self.canvas["world_resolution"] = [float(x) for x in self.world_texture.size]
win_rc = Window.render_context
self.canvas["projection_mat"] = win_rc["projection_mat"]
self.canvas["modelview_mat"] = win_rc["modelview_mat"]
self.canvas["frag_modelview_mat"] = win_rc["frag_modelview_mat"]
def update_world(self, *args):
H = len(self.world)
W = len(self.world[0]) if H else 0
texture = Texture.create(size=(W, H))
texture.min_filter = 'nearest'
texture.mag_filter = 'nearest'
# use r and g channel to contain x and y of tile id
buff = []
for line in self.world:
for cell in line:
buff.extend([
cell[0], # R: x of tile_id
cell[1], # G: y of tile_id
0, # B not used
])
texture.blit_buffer(
bytes(buff), colorfmt='rgb', bufferfmt='ubyte'
)
with self.canvas:
BindTexture(texture=texture, index=1)
self.world_texture = texture
'''Example application using Shiled a tile map implementation in a shader.
Overworld.png comes from https://opengameart.org/content/zelda-like-tilesets-and-sprites and is licenced as CC-0
'''
from random import randint
from kivy.app import App
from kivy.lang import Builder
from kivy import properties as P
from kivy.animation import Animation
import shiled
KV = '''
FloatLayout:
padding: 100
spacing: 100
ShileSet:
source: 'Overworld.png'
source_resolution: 640, 576
tile_size: 16, 16
world: app.world
zoom: app.zoom
offset: app.offset
ShileSet:
id: map2
source: 'Overworld.png'
source_resolution: 640, 576
tile_size: 16, 16
world: app.world
zoom: app.zoom2
offset: app.offset2
'''
class Application(App):
world = P.ListProperty()
offset = P.ListProperty([0, 0])
offset2 = P.ListProperty([0, 0])
zoom = P.NumericProperty(4.)
zoom2 = P.NumericProperty(2.)
def build(self):
self.world = [
[
[randint(1, 40), randint(1, 40)]
for cell in range(200)
] for line in range(100)
]
a = (
Animation(offset=(0, -1000), duration=5)
+ Animation(offset=(-1000, -1000), duration=5)
+ Animation(offset=(-1000, 0), duration=5)
+ Animation(offset=(0, 0), duration=5)
)
a.repeat = True
a.start(self)
root = Builder.load_string(KV)
root.bind(on_touch_move=self.handle_move, on_touch_down=self.handle_down)
return root
def handle_down(self, root, touch):
if 'button' in touch.profile and touch.button.startswith("scroll"):
self.zoom2 *= (0.9 if touch.button.endswith("down") else 1.1)
self.zoom2 = min(8, max(0.5, self.zoom2))
def handle_move(self, root, touch):
if root.ids.map2.collide_point(*touch.pos):
ox = self.offset2[0] + touch.dx
oy = self.offset2[1] + touch.dy
self.offset2 = [ox, oy]
if __name__ == "__main__":
Application().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment