add background blur

This commit is contained in:
Rokas Puzonas 2024-12-29 03:03:00 +02:00
parent bbdf0511b9
commit b80a356739
4 changed files with 597 additions and 64 deletions

442
gui/app.zig Normal file
View File

@ -0,0 +1,442 @@
// zig fmt: off
const std = @import("std");
const Api = @import("artifacts-api");
const Artificer = @import("artificer");
const UI = @import("./ui.zig");
const UIStack = @import("./ui_stack.zig");
const RectUtils = @import("./rect_utils.zig");
const rl = @import("raylib");
const srcery = @import("./srcery.zig");
const assert = std.debug.assert;
const App = @This();
const MapTexture = struct {
name: Api.Map.Skin,
texture: rl.Texture2D,
};
ui: UI,
artificer: *Artificer,
map_textures: std.ArrayList(MapTexture),
map_texture_indexes: std.ArrayList(usize),
map_position_min: Api.Position,
map_position_max: Api.Position,
camera: rl.Camera2D,
blur_texture_original: ?rl.RenderTexture = null,
blur_texture_horizontal: ?rl.RenderTexture = null,
blur_texture_both: ?rl.RenderTexture = null,
blur_shader: rl.Shader,
pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App {
const store = artificer.server.store;
var map_textures = std.ArrayList(MapTexture).init(allocator);
errdefer map_textures.deinit();
errdefer {
for (map_textures.items) |map_texture| {
map_texture.texture.unload();
}
}
var map_texture_indexes = std.ArrayList(usize).init(allocator);
errdefer map_texture_indexes.deinit();
// Load all map textures from api store
{
const map_image_ids = store.images.category_mapping.get(.map).items;
try map_textures.ensureTotalCapacity(map_image_ids.len);
for (map_image_ids) |image_id| {
const image = store.images.get(image_id).?;
const texture = rl.loadTextureFromImage(rl.Image{
.width = @intCast(image.width),
.height = @intCast(image.height),
.data = store.images.getRGBA(image_id).?.ptr,
.mipmaps = 1,
.format = rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8
});
if (!rl.isTextureReady(texture)) {
return error.LoadMapTextureFromImage;
}
map_textures.appendAssumeCapacity(MapTexture{
.name = try Api.Map.Skin.fromSlice(image.code.slice()),
.texture = texture
});
}
}
var map_position_max = Api.Position.zero();
var map_position_min = Api.Position.zero();
for (store.maps.items) |map| {
map_position_min.x = @min(map_position_min.x, map.position.x);
map_position_min.y = @min(map_position_min.y, map.position.y);
map_position_max.x = @max(map_position_max.x, map.position.x);
map_position_max.y = @max(map_position_max.y, map.position.y);
}
const map_size = map_position_max.subtract(map_position_min);
try map_texture_indexes.ensureTotalCapacity(@intCast(map_size.x * map_size.y));
for (0..@intCast(map_size.y)) |oy| {
for (0..@intCast(map_size.x)) |ox| {
const x = map_position_min.x + @as(i64, @intCast(ox));
const y = map_position_min.y + @as(i64, @intCast(oy));
const map = store.getMap(.{ .x = x, .y = y }).?;
var found_texture = false;
const map_skin = map.skin.slice();
for (0.., map_textures.items) |i, map_texture| {
if (std.mem.eql(u8, map_skin, map_texture.name.slice())) {
map_texture_indexes.appendAssumeCapacity(i);
found_texture = true;
break;
}
}
if (!found_texture) {
return error.MapImageNotFound;
}
}
}
const blur_shader = rl.loadShaderFromMemory(
@embedFile("./base.vsh"),
@embedFile("./blur.fsh"),
);
if (!rl.isShaderReady(blur_shader)) {
return error.LoadShaderFromMemory;
}
return App{
.artificer = artificer,
.ui = UI.init(),
.map_textures = map_textures,
.map_texture_indexes = map_texture_indexes,
.map_position_max = map_position_max,
.map_position_min = map_position_min,
.blur_shader = blur_shader,
.camera = rl.Camera2D{
.offset = rl.Vector2.zero(),
.target = rl.Vector2.zero(),
.rotation = 0,
.zoom = 1,
}
};
}
pub fn deinit(self: *App) void {
for (self.map_textures.items) |map_texture| {
map_texture.texture.unload();
}
self.ui.deinit();
self.map_textures.deinit();
self.map_texture_indexes.deinit();
if (self.blur_texture_horizontal) |render_texture| {
render_texture.unload();
}
if (self.blur_texture_both) |render_texture| {
render_texture.unload();
}
if (self.blur_texture_original) |render_texture| {
render_texture.unload();
}
}
fn cameraControls(camera: *rl.Camera2D) void {
if (rl.isMouseButtonDown(.mouse_button_left)) {
const mouse_delta = rl.getMouseDelta();
camera.target.x -= mouse_delta.x / camera.zoom;
camera.target.y -= mouse_delta.y / camera.zoom;
}
const zoom_speed = 0.2;
const min_zoom = 0.1;
const max_zoom = 2;
const zoom_delta = rl.getMouseWheelMove();
if (zoom_delta != 0) {
const mouse_screen = rl.getMousePosition();
// Get the world point that is under the mouse
const mouse_world = rl.getScreenToWorld2D(mouse_screen, camera.*);
// Set the offset to where the mouse is
camera.offset = mouse_screen;
// Set the target to match, so that the camera maps the world space point
// under the cursor to the screen space point under the cursor at any zoom
camera.target = mouse_world;
// Zoom increment
camera.zoom *= (1 + zoom_delta * zoom_speed);
camera.zoom = std.math.clamp(camera.zoom, min_zoom, max_zoom);
}
}
// fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void {
// var buffer: [256]u8 = undefined;
//
// const name_height = 20;
// UI.drawTextCentered(ui.font, brain.name, .{ .x = RectUtils.center(rect).x, .y = rect.y + name_height / 2 }, 20, 2, srcery.bright_white);
//
// var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom);
// label_stack.gap = 4;
//
// const now = std.time.milliTimestamp();
// const cooldown = brain.cooldown(&artificer.server);
// const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s;
// const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{seconds_left});
// UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
//
// var task_label: []u8 = undefined;
// if (brain.task) |task| {
// task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{@tagName(task)});
// } else {
// task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{});
// }
// UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
//
// const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{brain.action_queue.items.len});
// UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
// }
fn createOrGetRenderTexture(maybe_render_texture: *?rl.RenderTexture) !rl.RenderTexture {
const screen_width = rl.getScreenWidth();
const screen_height = rl.getScreenHeight();
if (maybe_render_texture.*) |render_texture| {
if (render_texture.texture.width != screen_width or render_texture.texture.height != screen_height) {
render_texture.unload();
maybe_render_texture.* = null;
}
}
if (maybe_render_texture.* == null) {
const render_texture = rl.loadRenderTexture(screen_width, screen_height);
if (!rl.isRenderTextureReady(render_texture)) {
return error.LoadRenderTexture;
}
maybe_render_texture.* = render_texture;
}
return maybe_render_texture.*.?;
}
fn drawRectangleRoundedUV(rect: rl.Rectangle, roundness: f32, color: rl.Color) void {
if (roundness == 0) {
rl.drawRectangleRec(rect, color);
}
}
fn drawBlurredWorld(self: *App, rect: rl.Rectangle, roundness: f32, color: rl.Color) !void {
const blur_both = try createOrGetRenderTexture(&self.blur_texture_both);
const previous_texture = rl.getShapesTexture();
const previous_rect = rl.getShapesTextureRectangle();
defer rl.setShapesTexture(previous_texture, previous_rect);
const texture_height: f32 = @floatFromInt(blur_both.texture.height);
const shape_rect = rl.Rectangle{
.x = rect.x,
.y = texture_height - rect.y,
.width = rect.width,
.height = -rect.height,
};
rl.setShapesTexture(blur_both.texture, shape_rect);
drawRectangleRoundedUV(rect, roundness, 0, color);
}
pub fn drawWorld(self: *App) !void {
const screen_width = rl.getScreenWidth();
const screen_height = rl.getScreenHeight();
const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height));
const blur_original = try createOrGetRenderTexture(&self.blur_texture_original);
const blur_horizontal = try createOrGetRenderTexture(&self.blur_texture_horizontal);
const blur_both = try createOrGetRenderTexture(&self.blur_texture_both);
// 1 pass. Draw the all of the sprites
{
blur_original.begin();
defer blur_original.end();
rl.clearBackground(rl.Color.black.alpha(0));
self.camera.begin();
defer self.camera.end();
rl.drawCircleV(rl.Vector2.zero(), 5, rl.Color.red);
const map_size = self.map_position_max.subtract(self.map_position_min);
for (0..@intCast(map_size.y)) |oy| {
for (0..@intCast(map_size.x)) |ox| {
const map_index = @as(usize, @intCast(map_size.x)) * oy + ox;
const x = self.map_position_min.x + @as(i64, @intCast(ox));
const y = self.map_position_min.y + @as(i64, @intCast(oy));
const texture_index = self.map_texture_indexes.items[map_index];
const texture = self.map_textures.items[texture_index].texture;
const tile_size = rl.Vector2.init(224, 224);
const position = rl.Vector2.init(@floatFromInt(x), @floatFromInt(y)).multiply(tile_size);
rl.drawTextureV(texture, position, rl.Color.white);
}
}
}
// 2 pass. Apply horizontal blur
const kernel_radius: i32 = 16;
var kernel_coeffs: [kernel_radius * 2 + 1]f32 = undefined;
{
const sigma = 10;
for (0..kernel_coeffs.len) |i| {
const i_i32: i32 = @intCast(i);
const io: f32 = @floatFromInt(i_i32 - kernel_radius);
kernel_coeffs[i] = @exp(-(io * io) / (sigma * sigma));
// kernel_coeffs[i] /= @floatFromInt(kernel_coeffs.len);
}
var kernel_sum: f32 = 0;
for (kernel_coeffs) |coeff| {
kernel_sum += coeff;
}
for (&kernel_coeffs) |*coeff| {
coeff.* /= kernel_sum;
}
}
{
const texture_size_loc = rl.getShaderLocation(self.blur_shader, "textureSize");
assert(texture_size_loc != -1);
rl.setShaderValue(self.blur_shader, texture_size_loc, &screen_size, .shader_uniform_vec2);
}
{
const kernel_radius_loc = rl.getShaderLocation(self.blur_shader, "kernelRadius");
assert(kernel_radius_loc != -1);
rl.setShaderValue(self.blur_shader, kernel_radius_loc, &kernel_radius, .shader_uniform_int);
}
{
const coeffs_loc = rl.getShaderLocation(self.blur_shader, "coeffs");
assert(coeffs_loc != -1);
rl.setShaderValueV(self.blur_shader, coeffs_loc, &kernel_coeffs, .shader_uniform_float, kernel_coeffs.len);
}
{
const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection");
assert(kernel_direction_loc != -1);
const kernel_direction = rl.Vector2.init(1, 0);
rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2);
}
{
blur_horizontal.begin();
defer blur_horizontal.end();
rl.clearBackground(rl.Color.black.alpha(0));
self.blur_shader.activate();
defer self.blur_shader.deactivate();
rl.drawTextureRec(
blur_original.texture,
.{
.x = 0,
.y = 0,
.width = screen_size.x,
.height = -screen_size.y,
},
rl.Vector2.zero(),
rl.Color.white
);
}
// 3 pass. Apply vertical blur
{
const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection");
assert(kernel_direction_loc != -1);
const kernel_direction = rl.Vector2.init(0, 1);
rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2);
}
{
blur_both.begin();
defer blur_both.end();
self.blur_shader.activate();
defer self.blur_shader.deactivate();
rl.drawTextureRec(
blur_horizontal.texture,
.{
.x = 0,
.y = 0,
.width = screen_size.x,
.height = -screen_size.y,
},
rl.Vector2.zero(),
rl.Color.white
);
}
// Last thing, draw world without blur
rl.drawTextureRec(
blur_original.texture,
.{
.x = 0,
.y = 0,
.width = @floatFromInt(blur_original.texture.width),
.height = @floatFromInt(-blur_original.texture.height),
},
rl.Vector2.zero(),
rl.Color.white
);
}
pub fn tick(self: *App) !void {
try self.artificer.tick();
const screen_width = rl.getScreenWidth();
const screen_height = rl.getScreenHeight();
const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height));
cameraControls(&self.camera);
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(srcery.black);
try self.drawWorld();
try self.drawBlurredWorld(
.{ .x = 20, .y = 20, .width = 200, .height = 200 },
0.5,
rl.Color.gray
);
rl.drawFPS(
@as(i32, @intFromFloat(screen_size.x)) - 100,
@as(i32, @intFromFloat(screen_size.y)) - 24
);
// var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right);
// for (artificer.characters.items) |*brain| {
// const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len));
// try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain);
// }
}

24
gui/base.vsh Normal file
View File

@ -0,0 +1,24 @@
#version 330
// Input vertex attributes
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
in vec4 vertexColor;
// Input uniform values
uniform mat4 mvp;
// Output vertex attributes (to fragment shader)
out vec2 fragTexCoord;
out vec4 fragColor;
void main()
{
// Send vertex attributes to fragment shader
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
// Calculate final vertex position
gl_Position = mvp * vec4(vertexPosition, 1.0);
}

45
gui/blur.fsh Normal file
View File

@ -0,0 +1,45 @@
#version 330
#define MAX_KERNEL_RADIUS 16
#define MAX_KERNEL_COEFFS 2*MAX_KERNEL_RADIUS + 1
// Input vertex attributes (from vertex shader)
in vec2 fragTexCoord;
in vec4 fragColor;
// Input uniform values
uniform sampler2D texture0;
uniform vec4 colDiffuse;
uniform vec2 textureSize;
uniform float coeffs[MAX_KERNEL_COEFFS];
uniform int kernelRadius;
uniform vec2 kernelDirection;
// Output fragment color
out vec4 finalColor;
void main()
{
vec2 texel = 1.0 / textureSize;
vec4 texelColor = vec4(0);
float alphaCorrection = 0;
for (int i = 0; i < 2*kernelRadius + 1; i++)
{
vec2 offset = kernelDirection * vec2(i - kernelRadius, i - kernelRadius) * texel;
vec2 sampleCoord = fragTexCoord + offset;
if ((0 <= sampleCoord.x && sampleCoord.x <= 1) && (0 <= sampleCoord.y && sampleCoord.y <= 1)) {
vec4 sample = texture(texture0, sampleCoord);
texelColor += sample * coeffs[i];
alphaCorrection += sample.a * coeffs[i];
}
}
texelColor /= alphaCorrection;
finalColor = texelColor * colDiffuse * fragColor;
}

View File

@ -1,15 +1,65 @@
// zig fmt: off
const std = @import("std");
const Artificer = @import("artificer");
const Api = @import("artifacts-api");
const rl = @import("raylib");
const Allocator = std.mem.Allocator;
const raylib_h = @cImport({
@cInclude("stdio.h");
@cInclude("raylib.h");
});
const App = @import("./app.zig");
const srcery = @import("./srcery.zig");
pub const std_options = .{
.log_scope_levels = &[_]std.log.ScopeLevel{
.{ .scope = .api, .level = .info },
.{ .scope = .raylib, .level = .warn },
}
};
const UI = @import("./ui.zig");
const UIStack = @import("./ui_stack.zig");
const RectUtils = @import("./rect_utils.zig");
fn toRaylibTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel {
return switch (log_level) {
.err => rl.TraceLogLevel.log_error,
.warn => rl.TraceLogLevel.log_warning,
.info => rl.TraceLogLevel.log_info,
.debug => rl.TraceLogLevel.log_trace,
};
}
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
fn toZigLogLevel(log_type: c_int) ?std.log.Level {
return switch (log_type) {
@intFromEnum(rl.TraceLogLevel.log_trace) => std.log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_debug) => std.log.Level.debug,
@intFromEnum(rl.TraceLogLevel.log_info) => std.log.Level.info,
@intFromEnum(rl.TraceLogLevel.log_warning) => std.log.Level.warn,
@intFromEnum(rl.TraceLogLevel.log_error) => std.log.Level.err,
@intFromEnum(rl.TraceLogLevel.log_fatal) => std.log.Level.err,
else => null,
};
}
fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: [*c]raylib_h.struct___va_list_tag_1) callconv(.C) void {
const log_level = toZigLogLevel(logType) orelse return;
const scope = .raylib;
const raylib_log = std.log.scoped(scope);
const max_tracelog_msg_length = 256; // from utils.c in raylib
var buffer: [max_tracelog_msg_length:0]u8 = undefined;
inline for (std.meta.fields(std.log.Level)) |field| {
const message_level: std.log.Level = @enumFromInt(field.value);
if (std.log.logEnabled(message_level, scope) and log_level == message_level) {
@memset(&buffer, 0);
const text_length = raylib_h.vsnprintf(&buffer, buffer.len, text, args);
const formatted_text = buffer[0..@intCast(text_length)];
const log_function = @field(raylib_log, field.name);
@call(.auto, log_function, .{ "{s}", .{formatted_text} });
}
}
}
fn getAPITokenFromArgs(allocator: std.mem.Allocator) !?[]u8 {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
@ -22,37 +72,7 @@ fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
var token_buffer: [256]u8 = undefined;
const token = try cwd.readFile(filename, &token_buffer);
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
}
fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void {
var buffer: [256]u8 = undefined;
const name_height = 20;
UI.drawTextCentered(ui.font, brain.name, .{
.x = RectUtils.center(rect).x,
.y = rect.y + name_height/2
}, 20, 2, srcery.bright_white);
var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom);
label_stack.gap = 4;
const now = std.time.milliTimestamp();
const cooldown = brain.cooldown(&artificer.server);
const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s;
const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{ seconds_left });
UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
var task_label: []u8 = undefined;
if (brain.task) |task| {
task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{ @tagName(task) });
} else {
task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{ });
}
UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{ brain.action_queue.items.len });
UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white);
return try allocator.dupe(u8, std.mem.trim(u8, token, "\n\t "));
}
pub fn main() anyerror!void {
@ -60,45 +80,47 @@ pub fn main() anyerror!void {
defer _ = gpa.deinit();
const allocator = gpa.allocator();
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
rl.setTraceLogLevel(toRaylibTraceLogLevel(std.log.default_level));
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
var artificer = try Artificer.init(allocator, token);
var store = try Api.Store.init(allocator);
defer store.deinit(allocator);
var server = try Api.Server.init(allocator, &store);
defer server.deinit();
try server.setToken(token);
var artificer = try Artificer.init(allocator, &server);
defer artificer.deinit();
const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin");
defer allocator.free(cache_path);
std.log.info("Prefetching server data", .{});
try artificer.server.prefetchCached(cache_path);
{
const cwd_path = try std.fs.cwd().realpathAlloc(allocator, ".");
defer allocator.free(cwd_path);
const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store.bin" });
defer allocator.free(cache_path);
try server.prefetchCached(allocator, cache_path);
}
rl.initWindow(800, 450, "Artificer");
defer rl.closeWindow();
rl.setTargetFPS(60);
rl.setWindowMinSize(200, 200);
rl.setWindowState(.{
.vsync_hint = true,
// .window_resizable = true
});
var ui = UI.init();
defer ui.deinit();
var app = try App.init(allocator, &artificer);
defer app.deinit();
while (!rl.windowShouldClose()) {
if (std.time.milliTimestamp() > artificer.nextStepAt()) {
try artificer.step();
}
const screen_size = rl.Vector2.init(
@floatFromInt(rl.getScreenWidth()),
@floatFromInt(rl.getScreenHeight())
);
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(srcery.black);
var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right);
for (artificer.characters.items) |*brain| {
const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len));
try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain);
}
try app.tick();
}
}