Pixel Art Tileset Texture Mapping with Trim + Animation (Godot 4)

This shader builds on top of my other tileset texture mapping shaders by adding a trim along the side faces.

The trim is also based on a tileset image. 

The trim can also be animated by adding additional tile indexes to trim_front/trim_back/trim_left/trim_right.

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 / trim_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: Arrays of tile indexes to display, additional indexes beyond 1 will result in the texture animating.  0 is reserved and won't display any tile that might be in the top left corner of your tileset
  • trim_front/trim_back/trim_left/trim_right: Same as the above but for trim
  • animation_speed: the speed of the animation
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[10];
uniform int bottom[10];
uniform int front[10];
uniform int back[10];
uniform int left[10];
uniform int right[10];
uniform float animation_speed;
uniform sampler2D tileset_texture: source_color, filter_nearest, repeat_enable;
uniform sampler2D trim_texture: source_color, filter_nearest, repeat_enable;
uniform vec2 trim_tile_size = vec2(16.0, 16.0);
uniform int trim_front[10];
uniform int trim_back[10];
uniform int trim_left[10];
uniform int trim_right[10];


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;
	COLOR = vec4(0.1,0.1,0.1,0.0);
}

int getArraySize(int[10] arr) {
    int size = 0;
    for (int i = 0; i < arr.length(); i++) {
        if (arr[i] != 0) {
            size++;
        }
		else {
			break;
		}
    }
    return size;
}

vec4 get_base_tileset(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];
	
    int frontSize = getArraySize(front);
    int backSize = getArraySize(back);
    int topSize = getArraySize(top);
    int bottomSize = getArraySize(bottom);
    int leftSize = getArraySize(left);
    int rightSize = getArraySize(right);

    int frontFrame = int(mod(TIME * animation_speed, float(frontSize)));
    int backFrame = int(mod(TIME * animation_speed, float(backSize)));
    int topFrame = int(mod(TIME * animation_speed, float(topSize)));
    int bottomFrame = int(mod(TIME * animation_speed, float(bottomSize)));
    int leftFrame = int(mod(TIME * animation_speed, float(leftSize)));
    int rightFrame = int(mod(TIME * animation_speed, float(rightSize)));

    tile_offset[0] = vec2(mod(float(front[frontFrame]), tileset_dimensions.x), floor(float(front[frontFrame]) / tileset_dimensions.x)) * tile_uv_scale;
    tile_offset[1] = vec2(mod(float(back[backFrame]), tileset_dimensions.x), floor(float(back[backFrame]) / tileset_dimensions.x)) * tile_uv_scale;
    tile_offset[2] = vec2(mod(float(top[topFrame]), tileset_dimensions.x), floor(float(top[topFrame]) / tileset_dimensions.x)) * tile_uv_scale;
    tile_offset[3] = vec2(mod(float(bottom[bottomFrame]), tileset_dimensions.x), floor(float(bottom[bottomFrame]) / tileset_dimensions.x)) * tile_uv_scale;
    tile_offset[4] = vec2(mod(float(left[leftFrame]), tileset_dimensions.x), floor(float(left[leftFrame]) / tileset_dimensions.x)) * tile_uv_scale;
    tile_offset[5] = vec2(mod(float(right[rightFrame]), tileset_dimensions.x), floor(float(right[rightFrame]) / 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;
}

vec4 get_trim_tileset(vec3 world_position, vec3 world_normal){
    vec2 uv;
    int trim_index;

    int frontSize = getArraySize(trim_front);
    int backSize = getArraySize(trim_back);
    int leftSize = getArraySize(trim_left);
    int rightSize = getArraySize(trim_right);

    int frontFrame = int(mod(TIME * animation_speed, float(frontSize)));
    int backFrame = int(mod(TIME * animation_speed, float(backSize)));
    int leftFrame = int(mod(TIME * animation_speed, float(leftSize)));
    int rightFrame = int(mod(TIME * animation_speed, float(rightSize)));

    if (abs(world_normal.x) > 0.5) { // Left or right face
        uv = world_position.zy;
        trim_index = world_normal.x > 0.0 ? trim_right[rightFrame] : trim_left[leftFrame];
    } else if (abs(world_normal.z) > 0.5) { // Front or back face
        uv = world_position.xy;
        trim_index = world_normal.z > 0.0 ? trim_front[frontFrame] : trim_back[backFrame];
    }

    uv = uv / model_dimensions.xy + 0.5;
    uv.y = 1.0 - uv.y;
    uv *= texture_scale; 

    vec2 trim_tileset_dimensions = vec2(textureSize(trim_texture,0)) / trim_tile_size;
    vec2 trim_tile_uv_scale = trim_tile_size / vec2(textureSize(trim_texture,0));
    vec2 trim_tile_offset = vec2(mod(float(trim_index), trim_tileset_dimensions.x), floor(float(trim_index) / trim_tileset_dimensions.x)) * trim_tile_uv_scale;
    vec4 trim_color = texture(trim_texture, fract(uv) * trim_tile_uv_scale + trim_tile_offset);
	return trim_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 = get_base_tileset(inverse(mat3(model_matrix)) * world_position, inverse(mat3(model_matrix)) *  world_normal);
    vec4 trim_color = get_trim_tileset(inverse(mat3(model_matrix)) * world_position, inverse(mat3(model_matrix)) * world_normal);
    bool is_trim_fragment = trim_color.a > 0.1;
    bool is_on_target_face = abs(world_normal.x) > 0.5 || abs(world_normal.z) > 0.5;
    vec4 final_color = is_on_target_face && is_trim_fragment ? trim_color : albedo;
    if (final_color.a <= 0.1) {
        discard;
    }
    ALBEDO = final_color.rgb;
}
Tags
pixel-art, texture mapping, tileset, trim
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from lefty_spurlock

Animated Pixel Art Tileset Texture Mapping (Godot 4)

Pixel Art Tileset Texture Mapping (Godot 4)

Related shaders

Pixel Art Tileset Texture Mapping (Godot 4)

Animated Pixel Art Tileset Texture Mapping (Godot 4)

Pixel Art Wind Sway (MeshInstance3D, Godot 4.0)

Subscribe
Notify of
guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Stef
Stef
6 months ago

This shader is great, but why the top and bottom textures are centered? Can I apply an offset to them to make them appear like the trimmed one?
screenshot