Gametoy: A shadertoy-alike for making games

Making games is fun. Well, it's fun when you aren't fighting asset loaders, wondering why Godot renders so slowly on Android and the million other problems that come with game engines. This is why I enjoy making games in shadertoy. It's a lot of work, but you work with math and math is predictable, logical and does what it says on the tin.

Shadertoy has some limitations for creating games: a maximum of 4 buffers limited to the viewport resolution. If you could make your game more performant by one buffer being 32x32 pixels? tough. If you want 5 buffers so you can split your rendering cleanly from your logic? Nope, can't do that either. Then there's also the fact you can only deploy to the web. Want to send an executable to a friend or potentially sell it on Steam? Not going to happen.

A couple months ago I had the idea of a slightly more flexible Shadertoy-alike. It would be purely made out of buffered shaders to mantain the mathematical simplixity, but it would allow flexible configuration of those buffers. It would target WebGL and desktop. I drew sketches of what it could look like and then forgot about it. Then, two weeks back I was working on another game in Shadertoy and hit the buffer limit again. I suddenly realized I had all the tools I needed to build it, and in my mind the architecture had worked it's way out. There was nothing stopping me.

So, I fired up Rust, pulled in the Glow crate and set about working. Today I achived a major milestone: the first interactive shader. While it's not a game, it uses all the features that would be required to implement one.

So here we have it, the first "game" in GameToy:


Gametoy is configured by a JSON file. This file defines what shaders are used and the buffers that link them. Here's the one for the game above: { "metadata": { "game_name": "CaveX21", "game_version": "0.0.0", "author_name": "sdfgeoff", "website": "http://nowhere.com", "license": "CC-BY", "release_date": "2021-07-23" }, "graph": { "nodes": [ { "Keyboard":{ "name": "Keyboard" } }, { "RenderPass":{ "name": "State", "output_texture_slots": [ {"name": "fragColor", "format": "RGBA32F"} ], "input_texture_slots": [ {"name": "BUFFER_KEYBOARD"}, {"name": "BUFFER_MAP_STATE"}, {"name": "BUFFER_STATE"} ], "resolution_scaling_mode": {"Fixed":[8,8]}, "fragment_shader_paths": ["common.frag", "state.frag"], "execution_mode": "Always" } }, { "RenderPass":{ "name": "Map", "output_texture_slots": [ {"name": "fragColor", "format": "RGBA32F"} ], "input_texture_slots": [ {"name": "BUFFER_STATE"}, {"name": "BUFFER_MAP_STATE"} ], "resolution_scaling_mode": {"Fixed":[32,32]}, "fragment_shader_paths": ["common.frag", "map.frag"], "execution_mode": "Always" } }, { "RenderPass":{ "name": "Render", "output_texture_slots": [ {"name": "fragColor", "format": "RGBA8"} ], "input_texture_slots": [ {"name": "BUFFER_STATE"}, {"name": "BUFFER_MAP_STATE"} ], "resolution_scaling_mode": {"ViewportScale":[1.0,1.0]}, "fragment_shader_paths": ["common.frag", "render.frag"], "execution_mode": "Always" } }, { "Output": { "name": "Output" } } ], "links": [ { "start_node": "Keyboard", "start_output_slot": "tex", "end_node": "State", "end_input_slot": "BUFFER_KEYBOARD" }, { "start_node": "State", "start_output_slot": "fragColor", "end_node": "State", "end_input_slot": "BUFFER_STATE" }, { "start_node": "Map", "start_output_slot": "fragColor", "end_node": "State", "end_input_slot": "BUFFER_MAP_STATE" }, { "start_node": "Map", "start_output_slot": "fragColor", "end_node": "Map", "end_input_slot": "BUFFER_MAP_STATE" }, { "start_node": "State", "start_output_slot": "fragColor", "end_node": "Map", "end_input_slot": "BUFFER_STATE" }, { "start_node": "Map", "start_output_slot": "fragColor", "end_node": "Render", "end_input_slot": "BUFFER_MAP_STATE" }, { "start_node": "State", "start_output_slot": "fragColor", "end_node": "Render", "end_input_slot": "BUFFER_STATE" }, { "start_node": "Render", "start_output_slot": "fragColor", "end_node": "Output", "end_input_slot": "col" } ] } }

The JSON file is a pretty nice summary of the feature set so far.

The game reads all the data from a single ".tar" file. This means that even if you have lots of shader files + assets, it's still only a single request in WASM.

Working with the JSON file is a bit unweildy, so one of the future-plans is to make a minimal graph-editor for it. Other features I'd like to add before I make the code public include:

I also have some more distant ideas such as: Some of these are easily achievable but not critical. Others will require a bunch more thinking.

Overall this has been a pretty fun learning experience. I've learned a bunch about openGL, and as per normal I enjoy woring in rust!