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.
WRAP_MODE_REPEAT lets UV coordinates outside 0..1 tile the texture.CONSTANT_TYPE_USER material constant.CONSTANT_TYPE_TIME so Lua does not need to update the scroll offset every frame.The collection contains:
background game object with a Model componentcamera with camera component (orthographic) that just frames the example viewui game object with example UI elements (coins, texts)The repeating_background script exposes these properties:
tile_size: the intended on-screen size of one texture tile, in pixels.scroll_speed: the scroll speed in pixels per second. Only the x and y components are used.pattern_angle: the angle, in degrees, used to rotate the repeated texture pattern in the shader.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:
/example/repeating_background.vp/example/repeating_background.fptexture0 with Wrap U and Wrap V set to Repeatuv_params of type Userrotation_params of type Usertime_data of type Timeuv_params is packed as follows:
uv_params.x: how many tiles fit across the current window width.uv_params.y: how many tiles fit across the current window height.uv_params.z: horizontal scroll speed in UV tiles per second.uv_params.w: vertical scroll speed in UV tiles per second.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.
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.
WRAP_MODE_REPEAT.sprite/texture_scrolling for Sprite components, especially sprites using an atlas. The key material feature there is the Texture Transform 2D vertex attribute, which converts atlas UVs to local sprite UVs so scrolling stays inside the current atlas region.The asset used in this example is from Kenney’s Puzzle Pack 2, licensed under CC0.
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);
}