Skip to content

Instantly share code, notes, and snippets.

@partybusiness
Created April 2, 2024 21:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save partybusiness/9ee3f24f8d6aff0877e8103674a388da to your computer and use it in GitHub Desktop.
Save partybusiness/9ee3f24f8d6aff0877e8103674a388da to your computer and use it in GitHub Desktop.
Godot Wang Tile Shaders
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;
uniform sampler2D Tile_Texture:source_color;
//used for random values on tile
vec2 random(float x, float y) {
vec2 co = round(vec2(x,y));
return vec2(
fract(sin(dot(co.xy, vec2(3.3853, 89.1866))) * 263724.4767),
fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453)
);
}
bool xor(bool a, bool b) {
return ((a && !b) || (!a && b));
}
void fragment() {
const int numTiles = 8; //number of tiles displayed along 0..1 uv range
//round to nearest pixel for tile UV
vec2 uv = floor(UV*float(numTiles));
//get remainder UV for that pixel for the tile uv
vec2 tileUV = UV*float(numTiles) - uv;
//bottom and left of the tile is decided by sampleLocation
//right and top use offset samples to match those adjacent
vec2 randomval = random(uv.x,uv.y).gr;
bool sampleLocationx = randomval.x > 0.5;
bool sampleLocationy = randomval.y > 0.5;
bool sampleLocationRight = random(uv.x+1.0,uv.y).g > 0.5;
bool sampleLocationAbove = random(uv.x,uv.y+1.0).r > 0.5;
uv = (uv) / float(numTiles);
//index math is a little weird but it means all adjacent tiles in source tilemap are connected, which helps mip maps look good
int index = 0;
index += 1 * (xor(sampleLocationRight, sampleLocationx)?1:0);
index += 2 * (sampleLocationx?1:0);
index += 4 * (xor(sampleLocationAbove, !sampleLocationy)?1:0);
index += 8 * (sampleLocationAbove?1:0);
vec2 offset = vec2(float(index%4),float(index/4))/4.0;
vec2 scaling = UV * float(numTiles); //use original UVs for LOD calculation
float lod = max(0.0, log2(max(length(dFdx(scaling)), length(dFdy(scaling)) )));
vec4 col = textureLod(Tile_Texture, (tileUV / 4.0)+offset, lod);
ALBEDO = vec3(col.rgb);
}
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;
uniform sampler2D Tile_Texture:source_color; //texture of wang tiles displayed in final material
uniform sampler2D Noise_Texture:filter_nearest; //texture with random RG values for randomizing Wang tiles
//needed an xor function because Godot shaders don't seem to have that built in?
bool xor(bool a, bool b) {
return ((a && !b) || (!a && b));
}
void fragment() {
const int numTiles = 8; //number of tiles displayed along 0..1 uv range
const int noiseSize = 32; //number of pixels in the noise texture
//round to nearest pixel for tile UV
vec2 uv = floor(UV*float(numTiles));
//get remainder UV for that pixel for the tile uv
vec2 tileUV = UV*float(numTiles) - uv;
float pixelOffset = 1. / float(noiseSize);
uv = uv / float(numTiles*noiseSize) * float(numTiles)+ pixelOffset/2.0; // make uv select pixel on noise texture
//bottom and left of the tile is decided by sampleLocation
//right and top use offset samples to match those adjacent
vec2 sampledVal = texture(Noise_Texture, uv).xy;
bool sampleLocationx = sampledVal.x > 0.5;
bool sampleLocationy = sampledVal.y > 0.5;
bool sampleLocationRight = texture(Noise_Texture, (uv+vec2(pixelOffset,0.0))).x > 0.5;
bool sampleLocationAbove = texture(Noise_Texture, (uv+vec2(0.0,pixelOffset))).y > 0.5;
//index math is a little weird but it means all adjacent tiles in source tilemap are connected, which helps mip maps look good
int index = 0;
index += 1 * (xor(sampleLocationRight, sampleLocationx)?1:0);
index += 2 * (sampleLocationx?1:0);
index += 4 * (xor(sampleLocationy,!sampleLocationAbove)?1:0);
index += 8 * (sampleLocationAbove?1:0);
vec2 offset = vec2(float(index%4),float(index/4))/4.0; //offset to current Wang tile
vec2 scaling = UV * float(numTiles); //use original UVs for LOD calculation
float lod = max(0.0, log2(max(length(dFdx(scaling)), length(dFdy(scaling)) )));
vec4 col = textureLod(Tile_Texture, (tileUV / 4.0)+offset, lod);
ALBEDO = col.rgb;
}
[gd_resource type="VisualShader" load_steps=6 format=3 uid="uid://fno4pw1ppmbn"]
[ext_resource type="Script" path="res://WangTiles/wang_tile_shader_node.gd" id="1_dbsbf"]
[sub_resource type="VisualShaderNodeCustom" id="VisualShaderNodeCustom_5hy2l"]
initialized = true
script = ExtResource("1_dbsbf")
[sub_resource type="VisualShaderNodeTexture2DParameter" id="VisualShaderNodeTexture2DParameter_dxlme"]
parameter_name = "Tile_Texture"
texture_type = 1
[sub_resource type="VisualShaderNodeInput" id="VisualShaderNodeInput_2cpev"]
input_name = "uv"
[sub_resource type="VisualShaderNodeIntConstant" id="VisualShaderNodeIntConstant_mftoa"]
constant = 13
[resource]
code = "shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_schlick_ggx;
uniform sampler2D Tile_Texture : source_color;
// WangTile
//random function for wang tiles
vec2 random_wang(float x, float y) {
vec2 co = round(vec2(y,x));
return vec2(
fract(sin(dot(co.yx, vec2(3.3853, 89.1866))) * 263724.4767),
fract(sin(dot(co.yx, vec2(12.9898, 78.233))) * 43758.5453)
);
}
bool xor(bool a, bool b) {
return ((a && !b) || (!a && b));
}
void fragment() {
// Input:4
vec2 n_out4p0 = UV;
// IntConstant:5
int n_out5p0 = 13;
vec4 n_out2p0;
// WangTile:2
{
//round to nearest pixel for tile UV
vec2 uv = floor(n_out4p0*float(n_out5p0));
float u = (uv.x);
float v = (uv.y);
//get remainder UV for that pixel for the tile uv
vec2 tileUV = n_out4p0*float(n_out5p0) - uv;
//bottom and left of the tile is decided by sampleLocation
//right and top use offset samples to match those adjacent
vec2 randomval = random_wang(u,v).xy;
bool sampleLocationx = randomval.y > 0.5;
bool sampleLocationy = randomval.x > 0.5;
bool sampleLocationRight = random_wang(u+1.0,v).y > 0.5;
bool sampleLocationAbove = random_wang(u,v+1.0).x > 0.5;
uv = (uv) / float(n_out5p0);
//index math is a little weird but it means all adjacent tiles in source tilemap are connected, which helps mip maps look good
int index = 0;
index += 1 * (xor(sampleLocationRight, sampleLocationx)?1:0);
index += 2 * (sampleLocationx?1:0);
index += 4 * (xor(sampleLocationAbove, !sampleLocationy)?1:0);
index += 8 * (sampleLocationAbove?1:0);
vec2 offset = vec2(float(index%4),float(index/4))/4.0;
vec2 scaling =n_out4p0 * float(n_out5p0); //use original UVs for LOD calculation
float lod = max(0., log2(max(length(dFdx(scaling)), length(dFdy(scaling)) )));
vec4 col = textureLod(Tile_Texture, (tileUV / 4.0)+offset, lod); //sample tile texture
n_out2p0 = col;
}
// Output:0
ALBEDO = vec3(n_out2p0.xyz);
}
"
graph_offset = Vector2(-988.439, -205.786)
nodes/fragment/2/node = SubResource("VisualShaderNodeCustom_5hy2l")
nodes/fragment/2/position = Vector2(-300, 220)
nodes/fragment/3/node = SubResource("VisualShaderNodeTexture2DParameter_dxlme")
nodes/fragment/3/position = Vector2(-1600, -80)
nodes/fragment/4/node = SubResource("VisualShaderNodeInput_2cpev")
nodes/fragment/4/position = Vector2(-960, 120)
nodes/fragment/5/node = SubResource("VisualShaderNodeIntConstant_mftoa")
nodes/fragment/5/position = Vector2(-760, 420)
nodes/fragment/connections = PackedInt32Array(2, 0, 0, 0, 3, 0, 2, 1, 4, 0, 2, 0, 5, 0, 2, 2)
@tool
extends VisualShaderNodeCustom
class_name WangTileShaderNode
func _get_name():
return "WangTile"
func _get_category():
return "MyShaderNodes"
func _get_description():
return "Samples random Wang Tiles"
func _init():
pass
func _get_return_icon_type():
return VisualShaderNode.PORT_TYPE_VECTOR_4D
func _get_input_port_count():
return 3
func _get_input_port_name(port):
match port:
0:
return "UV"
1:
return "TileTexture"
2:
return "TextureSize"
return "useless"
func _get_input_port_type(port):
match port:
0:
return VisualShaderNode.PORT_TYPE_VECTOR_2D
1:
return VisualShaderNode.PORT_TYPE_SAMPLER
2:
return VisualShaderNode.PORT_TYPE_SCALAR_INT
return VisualShaderNode.PORT_TYPE_SCALAR
func _get_output_port_count():
return 1
func _get_output_port_name(port):
return "Colour"
func _get_output_port_type(port):
return VisualShaderNode.PORT_TYPE_VECTOR_4D
func _get_global_code(mode):
return """
//random function for wang tiles
vec2 random_wang(float x, float y) {
vec2 co = round(vec2(y,x));
return vec2(
fract(sin(dot(co.yx, vec2(3.3853, 89.1866))) * 263724.4767),
fract(sin(dot(co.yx, vec2(12.9898, 78.233))) * 43758.5453)
);
}
bool xor(bool a, bool b) {
return ((a && !b) || (!a && b));
}
"""
func _get_code(input_vars, output_vars, mode, type):
return "" \
+ " //round to nearest pixel for tile UV\n" \
+ " vec2 uv = floor(%s*float(%s));\n"%[input_vars[0],input_vars[2]] \
+ " float u = (uv.x);\n"\
+ " float v = (uv.y);\n"\
+ " //get remainder UV for that pixel for the tile uv\n" \
+ " vec2 tileUV = %s*float(%s) - uv;\n"%[input_vars[0],input_vars[2]] \
+ " \n" \
+ " //bottom and left of the tile is decided by sampleLocation\n" \
+ " //right and top use offset samples to match those adjacent\n" \
+ " vec2 randomval = random_wang(u,v).xy;\n" \
+ " bool sampleLocationx = randomval.y > 0.5;\n" \
+ " bool sampleLocationy = randomval.x > 0.5;\n" \
+ " bool sampleLocationRight = random_wang(u+1.0,v).y > 0.5;\n" \
+ " bool sampleLocationAbove = random_wang(u,v+1.0).x > 0.5;\n" \
+ " uv = (uv) / float(%s);\n"%[input_vars[2]] \
+ " //index math is a little weird but it means all adjacent tiles in source tilemap are connected, which helps mip maps look good\n" \
+ " int index = 0;\n" \
+ " index += 1 * (xor(sampleLocationRight, sampleLocationx)?1:0);\n" \
+ " index += 2 * (sampleLocationx?1:0);\n" \
+ " index += 4 * (xor(sampleLocationAbove, !sampleLocationy)?1:0);\n" \
+ " index += 8 * (sampleLocationAbove?1:0);\n" \
+ " vec2 offset = vec2(float(index%4),float(index/4))/4.0;\n" \
+ " vec2 scaling =%s * float(%s); //use original UVs for LOD calculation \n"%[input_vars[0], input_vars[2]] \
+ " float lod = max(0., log2(max(length(dFdx(scaling)), length(dFdy(scaling)) )));\n" \
+ " vec4 col = textureLod(%s, (tileUV / 4.0)+offset, lod); //sample tile texture \n "%[input_vars[1]] \
+ " %s = col;\n"%[output_vars[0]]
@partybusiness
Copy link
Author

A picture of it in action over here, plus the source tile I was using for that:

https://mastodon.gamedev.place/@flinflonimation/112203833709650573

It assumes your tiles are in a 4x4 texture, with two types of edges, I'll call them A and B. Each tile matches the tiles adjacent to it, so for example if a tile has a B edge on the bottom then the tile below it has to have a B edge on the top.

They're laid out like:

Left-most and right-most column have A on the right edge. Middle two columns have B on the right edge.
Two right-side columns have B on the left edge, two left-side columns have A on the left edge.
Top-most and bottom-most rows have A on top edge. Middle two rows have B on the top edge.
Top two rows have B on the bottom edge, bottom two rows have A on the top edge.

In practice, A and B don't need to mean the same thing horizontally and vertically. Any tile with B on the right edge should be able to match a B on the left edge, but it doesn't matter if they can match a B on the top edge, for example, because that will never happen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment