Pixel Art Tileset Texture Mapping (Godot 4)
**You can find a version that supports animated tiles here: https://godotshaders.com/shader/animated-pixel-art-tileset-texture-mapping-godot-4/?post_id=5098&new_post=true
This shader will map a different tile to each side of a mesh based on a tileset index. Works for uniform and non-uniform tiles including sloped tiles. Works with GridMaps.
Use Metric units on your preferred modelling software, haven’t tested using other units.
Shader Parameters:
model_dimensions: set to bounding box of model to stretch textures. Optionally set to base tile size to repeat textures. For instance if my base tile size is 1x1x1, but I have a tile model that is 2x1x1, I can keep this value as 1x1x1 to repeat textures, or set it to 2x1x1 to stretch textures
tileset_texture: the tileset you wish to use, tiles should be the same size and uniformly spaced
tile_size: the size of the tile in pixels
blend_tiles: leave false if your object has slopes
blend_sharpness: how sharp of a blend between tiles
top/bottom/front/back/left/right: the different tile indexes where 0 is the top_left of the tileset_texture
Optionally comment out the following line if you don’t want your faces to be double sided:
render_mode cull_disabled;
Shader code
shader_type spatial;
render_mode cull_disabled;
uniform vec3 model_dimensions = vec3(1.0, 1.0, 1.0);
uniform vec2 tile_size = vec2(16.0, 16.0);
uniform float texture_scale : hint_range(0.1, 10.0) = 1.0;
uniform bool blend_tiles = false;
uniform float blend_sharpness : hint_range(0.0, 10.0) = 2.0;
uniform int top;
uniform int bottom;
uniform int front;
uniform int back;
uniform int left;
uniform int right;
uniform sampler2D tileset_texture: source_color, filter_nearest, repeat_enable;
varying vec3 instance_pos;
varying mat4 model_matrix;
void vertex() {
instance_pos = MODEL_MATRIX[3].xyz; //Position of Instance, supports MultiMeshInstances in the use of GridMaps
model_matrix = MODEL_MATRIX;
}
vec4 triplanar_texture(vec3 world_position, vec3 normal) {
vec3 weights = abs(normal);
weights = pow(weights, vec3(blend_sharpness));
weights /= dot(weights, vec3(1.0));
if (!blend_tiles) {
float threshold = 0.7071;
vec3 equal_weights = vec3(0.0, 1.0, 0.0); // Higher weight for top texture
weights = mix(equal_weights, weights, step(threshold, max(weights.x, max(weights.y, weights.z))));
}
vec2 tileset_dimensions = vec2(textureSize(tileset_texture,0)) / tile_size;
vec2 tile_uv_scale = tile_size / vec2(textureSize(tileset_texture,0));
vec2 tile_offset[6];
tile_offset[0] = vec2(mod(float(front), tileset_dimensions.x), floor(float(front) / tileset_dimensions.x)) * tile_uv_scale;
tile_offset[1] = vec2(mod(float(back), tileset_dimensions.x), floor(float(back) / tileset_dimensions.x)) * tile_uv_scale;
tile_offset[2] = vec2(mod(float(top), tileset_dimensions.x), floor(float(top) / tileset_dimensions.x)) * tile_uv_scale;
tile_offset[3] = vec2(mod(float(bottom), tileset_dimensions.x), floor(float(bottom) / tileset_dimensions.x)) * tile_uv_scale;
tile_offset[4] = vec2(mod(float(left), tileset_dimensions.x), floor(float(left) / tileset_dimensions.x)) * tile_uv_scale;
tile_offset[5] = vec2(mod(float(right), tileset_dimensions.x), floor(float(right) / tileset_dimensions.x)) * tile_uv_scale;
float margin = 0.001;
vec4 colors[6];
vec2 uv;
uv = (-world_position.zy / model_dimensions.zy + 0.5 - vec2(-((model_dimensions.z - 1.0) / (2.0 * model_dimensions.z)))) * texture_scale;
colors[0] = texture(tileset_texture, fract(uv) * tile_uv_scale + tile_offset[0]);
uv = (-world_position.zy / model_dimensions.zy + 0.5 - vec2(-((model_dimensions.z - 1.0) / (2.0 * model_dimensions.z)))) * texture_scale;
colors[1] = texture(tileset_texture, fract(uv) * tile_uv_scale + tile_offset[1]);
uv = (world_position.zx / model_dimensions.zx + 0.5 + vec2(-((model_dimensions.z - 1.0) / (2.0 * model_dimensions.z)), -((model_dimensions.x - 1.0) / (2.0 * model_dimensions.x)))) * texture_scale;
colors[2] = texture(tileset_texture, fract(uv) * tile_uv_scale + tile_offset[2]);
uv = (world_position.zx / model_dimensions.zx + 0.5 + vec2(-((model_dimensions.z - 1.0) / (2.0 * model_dimensions.z)), -((model_dimensions.x - 1.0) / (2.0 * model_dimensions.x)))) * texture_scale;
colors[3] = texture(tileset_texture, fract(uv) * tile_uv_scale + tile_offset[3]);
uv = (-world_position.xy / model_dimensions.xy + 0.5 + vec2(((model_dimensions.x - 1.0) / (2.0 * model_dimensions.x)),((model_dimensions.y - 1.0) / (2.0 * model_dimensions.y)))) * texture_scale;
colors[4] = texture(tileset_texture, fract(uv) * tile_uv_scale + tile_offset[4]);
uv = (-world_position.xy / model_dimensions.xy + 0.5 + vec2(((model_dimensions.x - 1.0) / (2.0 * model_dimensions.x)),((model_dimensions.y - 1.0) / (2.0 * model_dimensions.y)))) * texture_scale;
colors[5] = texture(tileset_texture, fract(uv) * tile_uv_scale + tile_offset[5]);
vec4 color = weights.x * (normal.x > 0.0 ? colors[0] : colors[1]) +
weights.y * (normal.y > 0.0 ? colors[2] : colors[3]) +
weights.z * (normal.z > 0.0 ? colors[4] : colors[5]);
return color;
}
void fragment() {
vec3 world_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz - instance_pos;
vec3 world_normal = (INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz;
vec4 albedo = triplanar_texture(inverse(mat3(model_matrix)) * world_position,inverse(mat3(model_matrix)) * world_normal);
if (albedo.a == 0.0){
discard;}
ALBEDO = albedo.rgb;
}