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:
Features
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 ability to create renderpasses, to configure their input/output textures and formats.
- The ability to pass data between renderpasses
- The ability for renderpasses to be cyclic/self-referential to allow state-preservation
- The ability to read from the keyboard
- The ability to draw to the screen/li>
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:
- Loading textures from disk (eg png files)
- Mouse/touch input
- Uniforms for input buffer resolutions
- Add user shaders to the Output Node
- Documentation!!!
- Only running buffers if their inputs have updated (to allow procedurally generated textures that don't impact performance)
- A web-based IDE similar to shadertoy's
- More configuration of renderpasses (Mips, filtering modes)
- Output nodes that "control" the engine (eg play/pause and save/load)
- Support for playing/generating sounds
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!