Repeating Background

Create a full-screen scrolling background using a repeated texture on a glTF quad.

Project files

A repeating, scrolling texture is useful for reward screens, menu backgrounds, water, clouds, stars, and other effects where a small tile should fill a large area. This example uses a Model component with Defold’s built-in /builtins/assets/gltf/quad.gltf mesh, a custom material, and a texture sampler set to repeat.

What You’ll Learn

Setup

The collection contains:

The repeating_background script exposes these properties:

The background game object itself is not rotated. Rotating the game object would rotate the screen-covering quad, so the visible result would be a rotated rectangle clipped by the viewport. To keep the background full-screen, leave the quad axis-aligned and rotate the UV coordinates in the fragment shader instead.

The model uses built-in quad.gltf, assigns repeating_background.material, and binds a bg_icon.png image to the material sampler named texture0.

The material has these important settings:

uv_params is packed as follows:

rotation_params stores cos(angle) and sin(angle) for the UV rotation. The shader uses these values to rotate the repeated texture coordinates inside the quad.

How It Works

This example uses Defold’s built-in glTF quad asset and #version 140 shader syntax to use a modern rendering pipeline.

The scrolling offset is calculated with the passing time, received automatically from the built-in Time material constant, so the script does not need an update() callback just to animate the texture (available since Defold 1.12.3).

repeating_background.script reads the window size on startup, scales the one-unit glTF quad to cover the viewport, converts the window size to a repeat scale, and sends uv_params and rotation_params to the Model component. It also registers a window listener so the layout is recalculated only when the window is resized.

The shader does the continuous motion. The vertex shader only transforms the quad and forwards texcoord0. The fragment shader centers the UVs around the middle of the quad, scales them by uv_params.xy, rotates them with rotation_params, subtracts a time-based scroll offset, and samples texture0 using repeated UV coordinates.

Because the scrolling offset is calculated from the engine-provided Time constant, no update() callback is needed just to animate the material. Lua only changes material data when the screen size or exposed properties determine a new repeat scale.

pattern_angle rotates the texture pattern and its local U/V axes. After the pattern is rotated, scroll_speed.x scrolls along the rotated U axis and scroll_speed.y scrolls along the rotated V axis. For example, keep scroll_speed at (50, 0, 0) and change pattern_angle to rotate a one-direction scrolling pattern; change both x and y in scroll_speed for diagonal movement in the pattern’s local space.

This example and sprite/texture_scrolling both animate UV coordinates with the Time material constant, but they solve different problems.

Credits

The asset used in this example is from Kenney’s Puzzle Pack 2, licensed under CC0.

Scripts

repeating_background.script

go.property("tile_size", 128) -- <1>
go.property("scroll_speed", vmath.vector3(50, 0, 0)) -- <2>
go.property("pattern_angle", 15) -- <3>

local function apply_layout(self, width, height)
	go.set(".", "scale", vmath.vector3(width, height, 1)) -- <4>

	local uv_params = vmath.vector4(
		width / self.tile_size,
		height / self.tile_size,
		self.scroll_speed.x / self.tile_size,
		self.scroll_speed.y / self.tile_size
	) -- <5>
	go.set("#model", "uv_params", uv_params) -- <6>

	local angle = math.rad(self.pattern_angle)
	local rotation_params = vmath.vector4(math.cos(angle), math.sin(angle), 0, 0) -- <7>
	go.set("#model", "rotation_params", rotation_params) -- <8>
end

local function on_window_event(self, event, data)
	if event == window.WINDOW_EVENT_RESIZED then
		apply_layout(self, data.width, data.height) -- <9>
	end
end

function init(self)
	local width, height = window.get_size()
	apply_layout(self, width, height) -- <10>
	window.set_listener(on_window_event) -- <11>
end

function final(self)
	window.set_listener(nil) -- <12>
end

--[[
1. `tile_size` is the size, in pixels, of one repeated texture tile on screen.
2. `scroll_speed` is measured in pixels per second. Only x and y are used.
3. `pattern_angle` rotates the repeated texture coordinates inside the quad. It does not rotate the game object.
4. The glTF quad is one unit wide and high, so scaling the game object to the window size makes it cover the viewport.
5. The shader receives repeat scale in `uv_params.xy` and normalized scroll speed in `uv_params.zw`.
6. `uv_params` is a user material constant on the Model component.
7. Store cosine and sine so the shader can rotate UV coordinates without recalculating trigonometry per pixel.
8. `rotation_params` is another user material constant on the Model component.
9. Recalculate the layout only when the window size changes.
10. Apply the initial layout before the first frame is rendered.
11. Listen for resize events instead of recalculating the layout every frame.
12. Clear the window listener when the script is finalized.
]]

repeating_background.vp

#version 140

in vec4 position;
in vec2 texcoord0;

uniform vs_uniforms
{
	mat4 mtx_worldview;
	mat4 mtx_proj;
};

out vec2 var_texcoord0;

void main()
{
	var_texcoord0 = texcoord0;
	gl_Position = mtx_proj * mtx_worldview * vec4(position.xyz, 1.0);
}

repeating_background.fp

#version 140

in mediump vec2 var_texcoord0;

out vec4 out_fragColor;

uniform mediump sampler2D texture0;

uniform fs_uniforms
{
	vec4 uv_params;
	vec4 rotation_params;
	vec4 time_data;
};

void main()
{
	vec2 repeat_scale = uv_params.xy;
	vec2 scroll_speed = uv_params.zw;
	vec2 scroll_offset = fract(time_data.x * scroll_speed);
	vec2 centered_uv = (var_texcoord0 - 0.5) * repeat_scale;
	vec2 rotated_uv = vec2(
		centered_uv.x * rotation_params.x - centered_uv.y * rotation_params.y,
		centered_uv.x * rotation_params.y + centered_uv.y * rotation_params.x
	);
	vec2 uv = rotated_uv - scroll_offset;
	out_fragColor = texture(texture0, uv);
}