We want to draw repeating tiled parallax background. The textures for backgrounds will be in a single background texture atlas, and we will sample sub-region into that atlas with repeating fashion that covers some part of the world area.
Repeating Parallax shader setup with 2 Constant buffers
Here’s the shader:
struct VS_INPUT {
float2 world_pos: POSITION;
};
struct VS_OUTPUT {
float4 clip_pos: SV_POSITION;
float2 camera_relative_world_pos: TEXCOORD0; // feeds frac() in PS
};
cbuffer CameraBuffer : register(b0) {
matrix view_projection;
};
cbuffer TileParams: register(b1) {
float4 atlas_rect;
float2 tile_size_world;
float2 parallax_factor;
float2 camera_pos;
};
VS_OUTPUT VSMain(VS_INPUT input) {
VS_OUTPUT output;
output.clip_pos = mul(view_projection, float4(input.world_pos, 0.0, 1.0));
output.camera_relative_world_pos = input.world_pos - camera_pos; // parallax-scaled camera pos from cbuffer
return output;
}
Texture2D tex : register(t0);
SamplerState samp : register(s0);
float4 PSMain(VS_OUTPUT input) : SV_TARGET
{
float2 tile_space = input.camera_relative_world_pos.xy / tile_size_world;
float2 local_uv = frac(tile_space);
float2 atlas_uv = lerp(atlas_rect.xy, atlas_rect.zw, local_uv);
float4 color = tex.Sample(samp, atlas_uv);
//color = float4(atlas_uv, 1.0, 1.0);
return color;
}
Here’s the corresponding Zig structs, mind the padding required (apparently Direct3D11 requires 16 byte aligned data for constant buffers):
const TileVertex = extern struct {
world_pos: [2]f32,
};
const TileParams = extern struct {
atlas_rect: [4]f32,
tile_size_world: [2]f32,
parallax_factor: [2]f32,
camera_pos: [2]f32,
_pad: [2]f32 = .{ 0, 0 },
};
We define our backgrounds at setup time:
const TileBackground = struct {
coverage: Rect,
atlas_rect_uv: [4]f32,
tile_size_world: [2]f32,
parallax_factor: [2]f32,
};
const MyWorldResources = struct {
const Self = @This();
backgrounds: [MyParallaxBackgroundRenderer.MaxBackgrounds]TileBackground,
pub fn deinit(self: *Self) void {
_ = self;
}
pub fn init(atlas: MyTexture) Self {
var backgrounds: [MyParallaxBackgroundRenderer.MaxBackgrounds]TileBackground = undefined;
backgrounds[0] = makeBackground(
.{ .x = 0, .y = 0, .width = 32, .height = 32 },
atlas,
.{
.x = 0,
.y = 0,
.width = 1000,
.height = 1000,
},
.{ 320, 320 },
.{ 0.5, 0.5 },
);
return .{ .backgrounds = backgrounds };
}
fn makeBackground(pixel_rect: Rect, atlas: MyTexture, coverage: Rect, tile_size: [2]f32, parallax: [2]f32) TileBackground {
const atlas_rect_uv = [4]f32{
pixel_rect.x / @as(f32, @floatFromInt(atlas.Width)),
pixel_rect.y / @as(f32, @floatFromInt(atlas.Height)),
(pixel_rect.x + pixel_rect.width) / @as(f32, @floatFromInt(atlas.Width)),
(pixel_rect.y + pixel_rect.height) / @as(f32, @floatFromInt(atlas.Height)),
};
return .{
.coverage = coverage,
.atlas_rect_uv = atlas_rect_uv,
.parallax_factor = parallax,
.tile_size_world = tile_size,
};
}
};
For rendering, we upload the vertices and indexes once for every background and never change them with .IMMUTABLE.
These vertices are uploaded once.
var all_vertices: [MaxBackgrounds * 4]TileVertex = undefined;
for (backgrounds, 0..) |bg, i| {
all_vertices[i * 4 + 0] = .{ .world_pos = .{ bg.coverage.x, bg.coverage.y } }; // top-left
all_vertices[i * 4 + 1] = .{ .world_pos = .{ bg.coverage.x + bg.coverage.width, bg.coverage.y } }; // top-right
all_vertices[i * 4 + 2] = .{ .world_pos = .{ bg.coverage.x + bg.coverage.width, bg.coverage.y + bg.coverage.height } }; // bottom-right
all_vertices[i * 4 + 3] = .{ .world_pos = .{ bg.coverage.x, bg.coverage.y + bg.coverage.height } }; // bottom-left
}
The indexes are same as the sprite batch version, and also uploaded once and never change.
We use DrawIndexed and call it with the offset into the related background section like this:
pub fn drawBackground(self: *Self, bg_index: u32, bg: TileBackground, camera: Camera) !void {
try self.PerFrameUploadLayer(bg, camera);
self.context.DrawIndexed(6, 0, @intCast(bg_index * 4));
}
Also let me show you the PerFrameUploadLayer to give you some idea of related bindings we have to set every frame necessary:
fn PerFrameUploadLayer(self: *Self, bg: TileBackground, camera: Camera) !void {
var mapped: D3D11_MAPPED_SUBRESOURCE = undefined;
var hr = self.context.Map(
@ptrCast(self.cbuffer),
0,
D3D11_MAP_WRITE_DISCARD,
0,
&mapped,
);
if (hr != HRESULT.S_OK) return error.MapFailed;
const params: *TileParams = @ptrCast(@alignCast(mapped.pData));
params.* = TileParams{
.atlas_rect = bg.atlas_rect_uv,
.tile_size_world = bg.tile_size_world,
.parallax_factor = bg.parallax_factor,
.camera_pos = .{
camera.position[0] * bg.parallax_factor[0],
camera.position[1] * bg.parallax_factor[1],
},
};
self.context.Unmap(@ptrCast(self.cbuffer), 0);
hr = self.context.Map(
@ptrCast(self.cbuffer2),
0,
D3D11_MAP_WRITE_DISCARD,
0,
&mapped,
);
if (hr != HRESULT.S_OK) return error.MapFailed;
const dest: *CameraConstants = @ptrCast(@alignCast(mapped.pData));
dest.* = CameraConstants{ .view_projection = camera.viewProjectionMatrix(game_width, game_height) };
self.context.Unmap(@ptrCast(self.cbuffer2), 0);
var pp_cbuffer = [_]?*ID3D11Buffer{
self.cbuffer2,
self.cbuffer,
};
self.context.VSSetConstantBuffers(0, 2, @ptrCast(&pp_cbuffer));
self.context.PSSetConstantBuffers(0, 2, @ptrCast(&pp_cbuffer));
}
Now I haven’t tested this with multiple backgrounds, but hopefully it shall work. You can find the tagged source code at v0.7.0.