Files
Zivro/src/render/cpu/pipeline.zig

350 lines
16 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const std = @import("std");
const dvui = @import("dvui");
const basic_models = @import("../../models/basic_models.zig");
const Document = @import("../../models/Document.zig");
const Point2_f = basic_models.Point2_f;
const Point2_i = basic_models.Point2_i;
const Scale2_f = basic_models.Scale2_f;
const Rect_i = basic_models.Rect_i;
const Color = dvui.Color;
/// Трансформ объекта: позиция, угол, масштаб, непрозрачность.
pub const Transform = struct {
position: Point2_f = .{},
angle: f32 = 0,
scale: Scale2_f = .{},
opacity: f32 = 1.0,
pub fn init(obj: *const Document.Object) Transform {
const pos = obj.getProperty(.position) orelse Point2_f{ .x = 0, .y = 0 };
const angle = obj.getProperty(.angle) orelse 0;
const scale = obj.getProperty(.scale) orelse Scale2_f{ .scale_x = 1, .scale_y = 1 };
const opacity = obj.getProperty(.opacity) orelse 1.0;
return .{
.position = pos,
.angle = angle,
.scale = scale,
.opacity = opacity,
};
}
/// Композиция: world = parent * local.
pub fn compose(parent: Transform, local: Transform) Transform {
const cos_a = std.math.cos(parent.angle);
const sin_a = std.math.sin(parent.angle);
const sx = parent.scale.scale_x * local.scale.scale_x;
const sy = parent.scale.scale_y * local.scale.scale_y;
const local_px = local.position.x * parent.scale.scale_x;
const local_py = local.position.y * parent.scale.scale_y;
const rx = cos_a * local_px - sin_a * local_py;
const ry = sin_a * local_px + cos_a * local_py;
return .{
.position = .{
.x = parent.position.x + rx,
.y = parent.position.y + ry,
},
.angle = parent.angle + local.angle,
.scale = .{ .scale_x = sx, .scale_y = sy },
.opacity = parent.opacity * local.opacity,
};
}
};
/// Мировые -> локальные для заданного трансформа.
pub fn worldToLocalTransform(t: Transform, wx: f32, wy: f32) Point2_f {
const dx = wx - t.position.x;
const dy = wy - t.position.y;
const cos_a = std.math.cos(-t.angle);
const sin_a = std.math.sin(-t.angle);
const sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0;
const sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0;
return .{
.x = (dx * cos_a - dy * sin_a) / sx,
.y = (dx * sin_a + dy * cos_a) / sy,
};
}
/// Конвейер отрисовки: локальные координаты -> трансформ -> буфер.
pub const DrawContext = struct {
pixels: []Color.PMA,
buf_width: u32,
buf_height: u32,
visible_rect: Rect_i,
scale_x: f32,
scale_y: f32,
transform: Transform = .{},
/// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда
replace_mode: bool = false,
_fill_canvas: ?*FillCanvas = null,
pub fn setTransform(self: *DrawContext, t: Transform) void {
self.transform = t;
}
/// Локальные -> мировые.
pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2_f {
const t = &self.transform;
const cos_a = std.math.cos(t.angle);
const sin_a = std.math.sin(t.angle);
return .{
.x = t.position.x + (local_x * t.scale.scale_x) * cos_a - (local_y * t.scale.scale_y) * sin_a,
.y = t.position.y + (local_x * t.scale.scale_x) * sin_a + (local_y * t.scale.scale_y) * cos_a,
};
}
/// Мировые -> буфер (float).
pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
const canvas_x = wx * self.scale_x;
const canvas_y = wy * self.scale_y;
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
return .{
.x = canvas_x - vx,
.y = canvas_y - vy,
};
}
/// Мировые -> буфер (целые).
pub fn worldToBuffer(self: *const DrawContext, wx: f32, wy: f32) Point2_i {
const b = self.worldToBufferF(wx, wy);
return .{
.x = @intFromFloat(std.math.round(b.x)),
.y = @intFromFloat(std.math.round(b.y)),
};
}
/// Буфер -> мировые.
pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2_f {
const vx = @as(f32, @floatFromInt(self.visible_rect.x));
const vy = @as(f32, @floatFromInt(self.visible_rect.y));
const canvas_x = buf_x + vx;
const canvas_y = buf_y + vy;
const sx = if (self.scale_x != 0) self.scale_x else 1.0;
const sy = if (self.scale_y != 0) self.scale_y else 1.0;
return .{
.x = canvas_x / sx,
.y = canvas_y / sy,
};
}
/// Мировые -> локальные.
pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
return worldToLocalTransform(self.transform, wx, wy);
}
/// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.
/// Если активен fill canvas, каждый записанный пиксель помечается как граница для заливки.
pub fn blendPixelAtBuffer(self: *DrawContext, bx_i32: i32, by_i32: i32, color: Color.PMA) void {
if (self._fill_canvas) |fc| fc.setBorder(bx_i32, by_i32);
if (bx_i32 < 0 or by_i32 < 0 or bx_i32 >= self.buf_width or by_i32 >= self.buf_height) return;
const bx: u32 = @intCast(bx_i32);
const by: u32 = @intCast(by_i32);
const idx = by * self.buf_width + bx;
const dst = &self.pixels[idx];
if (self.replace_mode) {
dst.* = color;
return;
}
const t = &self.transform;
const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity;
const src_r = @as(f32, @floatFromInt(color.r)) * t.opacity;
const src_g = @as(f32, @floatFromInt(color.g)) * t.opacity;
const src_b = @as(f32, @floatFromInt(color.b)) * t.opacity;
const inv_a = 1.0 - a;
dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255));
dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255));
dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255));
dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
}
/// Накладывает буфер другого контекста на этот с заданной прозрачностью (один бленд на пиксель). Размеры буферов должны совпадать.
pub fn compositeDrawerContext(self: *DrawContext, other: *const DrawContext, opacity: f32) void {
if (self.buf_width != other.buf_width or self.buf_height != other.buf_height) return;
const n = self.buf_width * self.buf_height;
for (0..n) |i| {
const src = other.pixels[i];
if (src.a == 0) continue;
const dst = &self.pixels[i];
const a = @as(f32, @floatFromInt(src.a)) / 255.0 * opacity;
const src_r = @as(f32, @floatFromInt(src.r)) * opacity;
const src_g = @as(f32, @floatFromInt(src.g)) * opacity;
const src_b = @as(f32, @floatFromInt(src.b)) * opacity;
const inv_a = 1.0 - a;
dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255));
dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255));
dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255));
dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255));
}
}
/// Пиксель в локальных координатах (трансформ + PMA).
pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void {
const w = self.localToWorld(local_x, local_y);
const b = self.worldToBufferF(w.x, w.y);
const bx: i32 = @intFromFloat(b.x);
const by: i32 = @intFromFloat(b.y);
const vw = @as(i32, @intCast(self.visible_rect.w));
const vh = @as(i32, @intCast(self.visible_rect.h));
if (bx < 0 or bx >= vw or by < 0 or by >= vh) return;
self.blendPixelAtBuffer(bx, by, color);
}
/// Начинает сбор границ для заливки: создаёт FillCanvas и при последующих вызовах blendPixelAtBuffer помечает пиксели как границу.
pub fn startFill(self: *DrawContext, allocator: std.mem.Allocator) !void {
const fc = try FillCanvas.init(allocator, self.buf_width, self.buf_height);
const ptr = try allocator.create(FillCanvas);
ptr.* = fc;
self._fill_canvas = ptr;
}
/// Рисует заливку по собранным границам цветом color, освобождает FillCanvas и сбрасывает режим.
pub fn stopFill(self: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void {
const fc = self._fill_canvas orelse return;
self._fill_canvas = null;
fc.fillColor(self, allocator, color);
fc.deinit();
allocator.destroy(fc);
}
};
/// Конвертирует u32 0xRRGGBBAA в Color.PMA.
pub fn rgbaToPma(rgba: u32) Color.PMA {
const r: u8 = @intCast((rgba >> 24) & 0xFF);
const g: u8 = @intCast((rgba >> 16) & 0xFF);
const b: u8 = @intCast((rgba >> 8) & 0xFF);
const a: u8 = @intCast((rgba >> 0) & 0xFF);
if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 };
const af: f32 = @as(f32, @floatFromInt(a)) / 255.0;
return .{
.r = @intFromFloat(@as(f32, @floatFromInt(r)) * af),
.g = @intFromFloat(@as(f32, @floatFromInt(g)) * af),
.b = @intFromFloat(@as(f32, @floatFromInt(b)) * af),
.a = a,
};
}
/// Контекст для заполнения фигур цветом. Границы хранятся в set — по x и y можно добавлять произвольные точки.
const FillCanvas = struct {
/// Множество пикселей границы (x, y) — без ограничения по размеру буфера.
border_set: std.AutoHashMap(Point2_i, void),
buf_width: u32,
buf_height: u32,
pub fn init(allocator: std.mem.Allocator, width: u32, height: u32) !FillCanvas {
const border_set = std.AutoHashMap(Point2_i, void).init(allocator);
return .{
.border_set = border_set,
.buf_width = width,
.buf_height = height,
};
}
pub fn deinit(self: *FillCanvas) void {
self.border_set.deinit();
}
/// Добавляет точку границы; координаты x, y могут быть любыми (условно бесконечное поле).
pub fn setBorder(self: *FillCanvas, x: i32, y: i32) void {
self.border_set.put(.{ .x = x, .y = y }, {}) catch {};
}
/// Заливка четырёхсвязным стековым алгоритмом от первой найденной внутренней точки.
pub fn fillColor(self: *FillCanvas, draw_ctx: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void {
const n = self.border_set.count();
if (n == 0) return;
const buf_w_i: i32 = @intCast(self.buf_width);
const buf_h_i: i32 = @intCast(self.buf_height);
// Ключи один раз по (y, x) — по строкам x уже будут отсортированы.
var keys_buf = std.ArrayList(Point2_i).empty;
defer keys_buf.deinit(allocator);
keys_buf.ensureTotalCapacity(allocator, n) catch return;
var iter = self.border_set.keyIterator();
while (iter.next()) |k| {
keys_buf.appendAssumeCapacity(k.*);
}
std.mem.sort(Point2_i, keys_buf.items, {}, struct {
fn lessThan(_: void, a: Point2_i, b: Point2_i) bool {
if (a.y != b.y) return a.y < b.y;
return a.x < b.x;
}
}.lessThan);
// Семена: по строкам находим сегменты (пары x), пересекаем с окном буфера, берём середину сегмента.
var seeds = findFillSeeds(keys_buf.items, buf_w_i, buf_h_i, allocator) catch return;
defer seeds.deinit(allocator);
var stack = std.ArrayList(Point2_i).empty;
defer stack.deinit(allocator);
var filled = std.AutoHashMap(Point2_i, void).init(allocator);
defer filled.deinit();
for (seeds.items) |s| {
if (self.border_set.contains(s)) continue;
if (filled.contains(s)) continue;
stack.clearRetainingCapacity();
stack.append(allocator, s) catch return;
while (stack.pop()) |cell| {
if (self.border_set.contains(cell)) continue;
const gop = filled.getOrPut(cell) catch return;
if (gop.found_existing) continue;
if (cell.x >= 0 and cell.x < buf_w_i and cell.y >= 0 and cell.y < buf_h_i) {
draw_ctx.blendPixelAtBuffer(cell.x, cell.y, color);
}
if (cell.x > 0) stack.append(allocator, .{ .x = cell.x - 1, .y = cell.y }) catch return;
if (cell.x < buf_w_i - 1) stack.append(allocator, .{ .x = cell.x + 1, .y = cell.y }) catch return;
if (cell.y > 0) stack.append(allocator, .{ .x = cell.x, .y = cell.y - 1 }) catch return;
if (cell.y < buf_h_i - 1) stack.append(allocator, .{ .x = cell.x, .y = cell.y + 1 }) catch return;
}
}
}
/// По строкам: рёбра (подряд идущие x) → сегменты между ними. Семена — середины чётных сегментов (при чётном числе границ).
fn findFillSeeds(
keys: []const Point2_i,
buf_w_i: i32,
buf_h_i: i32,
allocator: std.mem.Allocator,
) !std.ArrayList(Point2_i) {
var list = std.ArrayList(Point2_i).empty;
errdefer list.deinit(allocator);
var segments = std.ArrayList(struct { left: i32, right: i32 }).empty;
defer segments.deinit(allocator);
var i: usize = 0;
while (i < keys.len) {
const y = keys[i].y;
const row_start = i;
while (i < keys.len and keys[i].y == y) : (i += 1) {}
const row = keys[row_start..i];
if (row.len < 2 or y < 0 or y >= buf_h_i) continue;
segments.clearRetainingCapacity();
var run_end_x: i32 = row[0].x;
for (row[1..]) |p| {
if (p.x != run_end_x + 1) {
try segments.append(allocator, .{ .left = run_end_x + 1, .right = p.x - 1 });
run_end_x = p.x;
} else {
run_end_x = p.x;
}
}
// Семена только при чётном числе границ
if ((segments.items.len + 1) % 2 != 0) continue;
for (segments.items, 0..) |seg, gi| {
if (gi % 2 != 0 or seg.left > seg.right) continue;
const left = @max(seg.left, 0);
const right = @min(seg.right, buf_w_i - 1);
if (left <= right) {
try list.append(allocator, .{ .x = left + @divTrunc(right - left, 2), .y = y });
}
}
}
return list;
}
};