Первая растеризация
This commit is contained in:
148
src/render/cpu/pipeline.zig
Normal file
148
src/render/cpu/pipeline.zig
Normal file
@@ -0,0 +1,148 @@
|
||||
const std = @import("std");
|
||||
const dvui = @import("dvui");
|
||||
const basic_models = @import("../../models/basic_models.zig");
|
||||
const Point2 = basic_models.Point2;
|
||||
const Scale2 = basic_models.Scale2;
|
||||
const ImageRect = basic_models.ImageRect;
|
||||
const Color = dvui.Color;
|
||||
|
||||
/// Трансформ объекта в мировых координатах документа (позиция, угол, масштаб, непрозрачность).
|
||||
pub const Transform = struct {
|
||||
position: Point2 = .{},
|
||||
angle: f32 = 0,
|
||||
scale: Scale2 = .{},
|
||||
opacity: f32 = 1.0,
|
||||
|
||||
/// Композиция: мировой трансформ = parent * local (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 const DrawContext = struct {
|
||||
pixels: []Color.PMA,
|
||||
buf_width: u32,
|
||||
buf_height: u32,
|
||||
visible_rect: ImageRect,
|
||||
scale_x: f32,
|
||||
scale_y: f32,
|
||||
transform: Transform = .{},
|
||||
|
||||
pub fn setTransform(self: *DrawContext, t: Transform) void {
|
||||
self.transform = t;
|
||||
}
|
||||
|
||||
/// Локальные координаты фигуры -> мировые (документ).
|
||||
pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2 {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// Мировые координаты документа -> координаты в буфере (могут быть вне [0, buf_w] x [0, buf_h]).
|
||||
pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2 {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// Координаты буфера -> мировые (документ). scale_x/scale_y не должны быть 0.
|
||||
pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2 {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// Мировые координаты -> локальные фигуры (обратное к localToWorld).
|
||||
pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2 {
|
||||
const t = &self.transform;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// Смешивает цвет в пикселе буфера (bx, by) с учётом opacity текущего трансформа. Bounds не проверяются.
|
||||
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void {
|
||||
if (bx >= self.buf_width or by >= self.buf_height) return;
|
||||
const t = &self.transform;
|
||||
const idx = by * self.buf_width + bx;
|
||||
const dst = &self.pixels[idx];
|
||||
const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity;
|
||||
const src_r = @as(f32, @floatFromInt(color.r)) * a;
|
||||
const src_g = @as(f32, @floatFromInt(color.g)) * a;
|
||||
const src_b = @as(f32, @floatFromInt(color.b)) * a;
|
||||
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 blend).
|
||||
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(@intCast(bx), @intCast(by), color);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn rgbaToPma(rgba: u32) Color.PMA {
|
||||
const r: u8 = @intCast((rgba >> 0) & 0xFF);
|
||||
const g: u8 = @intCast((rgba >> 8) & 0xFF);
|
||||
const b: u8 = @intCast((rgba >> 16) & 0xFF);
|
||||
const a: u8 = @intCast((rgba >> 24) & 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user