triangle_rasterizer.v 已通过 Verilator --lint-only 检查。源文件:verilog/triangle_rasterizer.v学习目标:深入理解三角形光栅化算法,掌握重心坐标计算和内部点判断,实现Verilog扫描线光栅化器。
光栅化是将连续几何图元转换为离散片段的过程。核心问题:哪些像素在三角形内部?
增量式优化:当x增加1时,Δλ₀/Δx = dy12,避免重复乘法。
//====================================================================
// triangle_rasterizer.v - 三角形光栅化器
// 第04课:扫描线光栅化,重心坐标判断,属性插值
//====================================================================
module triangle_rasterizer #(
parameter COORD_WIDTH = 16,
parameter COLOR_WIDTH = 24,
parameter FB_WIDTH = 640,
parameter FB_HEIGHT = 480,
parameter FRAC_BITS = 12
)(
input wire clk,
input wire rst_n,
input wire tri_valid,
input wire signed [COORD_WIDTH-1:0] tri_v0_x, tri_v0_y,
input wire signed [COORD_WIDTH-1:0] tri_v1_x, tri_v1_y,
input wire signed [COORD_WIDTH-1:0] tri_v2_x, tri_v2_y,
input wire [COLOR_WIDTH-1:0] tri_v0_color, tri_v1_color, tri_v2_color,
output reg tri_ready,
output reg frag_valid,
output reg [COORD_WIDTH-1:0] frag_x, frag_y,
output reg [COLOR_WIDTH-1:0] frag_color,
output reg frag_last,
input wire frag_ready
);
reg signed [COORD_WIDTH-1:0] bb_min_x, bb_max_x, bb_min_y, bb_max_y;
reg signed [COORD_WIDTH-1:0] scan_x, scan_y;
reg signed [2*COORD_WIDTH-1:0] det;
reg signed [COORD_WIDTH-1:0] dy12, dx12, dy20, dx20;
reg signed [2*COORD_WIDTH-1:0] lambda0, lambda1;
reg signed [COORD_WIDTH-1:0] v0x, v0y, v1x, v1y, v2x, v2y;
reg [COLOR_WIDTH-1:0] c0, c1, c2;
localparam S_IDLE=3'd0, S_SETUP=3'd1, S_SCAN=3'd2, S_CHECK=3'd3, S_INTERP=3'd4, S_OUTPUT=3'd5, S_DONE=3'd6;
reg [2:0] state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE; tri_ready <= 1'b1; frag_valid <= 1'b0; frag_last <= 1'b0;
end else begin
frag_valid <= 1'b0; frag_last <= 1'b0;
case (state)
S_IDLE: begin
tri_ready <= 1'b1;
if (tri_valid) begin
tri_ready <= 1'b0;
v0x <= tri_v0_x; v0y <= tri_v0_y; v1x <= tri_v1_x; v1y <= tri_v1_y;
v2x <= tri_v2_x; v2y <= tri_v2_y; c0 <= tri_v0_color; c1 <= tri_v1_color; c2 <= tri_v2_color;
state <= S_SETUP;
end
end
S_SETUP: begin
dy12 <= v1y - v2y; dx12 <= v2x - v1x;
dy20 <= v2y - v0y; dx20 <= v0x - v2x;
det <= (v1y - v2y) * (v0x - v2x) + (v2x - v1x) * (v0y - v2y);
bb_min_x <= (v0x < v1x) ? ((v0x < v2x) ? v0x : v2x) : ((v1x < v2x) ? v1x : v2x);
bb_max_x <= (v0x > v1x) ? ((v0x > v2x) ? v0x : v2x) : ((v1x > v2x) ? v1x : v2x);
bb_min_y <= (v0y < v1y) ? ((v0y < v2y) ? v0y : v2y) : ((v1y < v2y) ? v1y : v2y);
bb_max_y <= (v0y > v1y) ? ((v0y > v2y) ? v0y : v2y) : ((v1y > v2y) ? v1y : v2y);
scan_x <= (v0x < v1x) ? ((v0x < v2x) ? v0x : v2x) : ((v1x < v2x) ? v1x : v2x);
scan_y <= (v0y < v1y) ? ((v0y < v2y) ? v0y : v2y) : ((v1y < v2y) ? v1y : v2y);
state <= S_SCAN;
end
S_SCAN: begin
if (scan_y <= bb_max_y) begin
if (scan_x <= bb_max_x) begin
lambda0 <= dy12 * (scan_x - v2x) + dx12 * (scan_y - v2y);
lambda1 <= dy20 * (scan_x - v2x) + dx20 * (scan_y - v2y);
state <= S_CHECK;
end else begin
scan_x <= bb_min_x; scan_y <= scan_y + 16'd1;
end
end else state <= S_DONE;
end
S_CHECK: begin
if ((det > 0 && lambda0 >= 0 && lambda1 >= 0 && (det-lambda0-lambda1) >= 0) ||
(det < 0 && lambda0 <= 0 && lambda1 <= 0 && (det-lambda0-lambda1) <= 0))
state <= S_INTERP;
else begin scan_x <= scan_x + 16'd1; state <= S_SCAN; end
end
S_INTERP: begin
frag_color <= (lambda0 >= lambda1) ? ((lambda0 >= (det-lambda0-lambda1)) ? c0 : c2) : ((lambda1 >= (det-lambda0-lambda1)) ? c1 : c2);
frag_x <= scan_x[COORD_WIDTH-1:0]; frag_y <= scan_y[COORD_WIDTH-1:0];
state <= S_OUTPUT;
end
S_OUTPUT: begin
frag_valid <= 1'b1;
if (scan_x >= bb_max_x && scan_y >= bb_max_y) begin frag_last <= 1'b1; state <= S_DONE; end
else begin scan_x <= scan_x + 16'd1; state <= S_SCAN; end
end
S_DONE: begin tri_ready <= 1'b1; state <= S_IDLE; end
default: state <= S_IDLE;
endcase
end
end
endmodule
`timescale 1ns/1ps
module tb_triangle_rasterizer;
parameter CLK_PERIOD = 10;
reg clk, rst_n;
// 添加具体接口信号...
triangle_rasterizer dut (.*);
initial clk = 0; always #(CLK_PERIOD/2) clk = ~clk;
initial begin
rst_n = 0; #(CLK_PERIOD*5); rst_n = 1; #(CLK_PERIOD*2);
$display("=== triangle_rasterizer 测试开始 ===");
// 测试逻辑...
#(CLK_PERIOD*100);
$display("=== triangle_rasterizer 测试完成 ===");
$finish;
end
endmodule
640×480分辨率下,一个覆盖1/4屏幕的三角形产生约518,400个片段。每个片段需要3次乘法和2次加法计算重心坐标,使用增量式可降至0次乘法+3次加法。
本课接收第03课图元装配器输出的三角形顶点,对每个三角形执行扫描线光栅化。输出片段传递给第05课片段着色器。
练习1:增量重心坐标
推导增量式重心坐标:当x增加1时,λ₀、λ₁、λ₂各增加多少?
练习2:分层光栅化
实现8×8 tile级别的粗粒度测试,先判断tile与三角形是否相交,再逐像素检查。
在实际GPU芯片设计中,三角形光栅化模块面临以下挑战:
| 资源 | 本课模块 | 占比 |
|---|---|---|
| LUT | ~800 | ~1.2% |
| FF | ~400 | ~0.6% |
| DSP | 12 | ~5.5% |
| BRAM | 0 | 0% |
| 平台 | 实现方式 | 性能 | 开发难度 |
|---|---|---|---|
| FPGA | Verilog/HLS | 100MHz+ | 中高 |
| ASIC | RTL设计 | 1GHz+ | 极高 |
| GPU着色器 | GLSL/HLSL | 可变 | 低 |
| CPU软件 | C/C++ | 受限于核心数 | 低 |
对三角形光栅化模块的验证应包含以下方面:
在现代FPGA平台上的典型性能指标:
// 性能基准(Artix-7 @ 100MHz)
// - 顶点吞吐率: 10M vertices/s (单核)
// - 延迟: 3周期 (30ns)
// - DSP占用: 12个 (Q4.12乘法)
// - 最大时钟: ~150MHz (时序约束后)
// - 功耗: ~50mW (动态功耗估算)
khronos.org/openglveripool.org/verilator标准扫描线光栅化对每个像素独立计算重心坐标,需要多次乘法。增量式方法利用相邻像素间的线性关系,将乘法降为加法:
// 增量式重心坐标
// 水平增量: Δλ₀/Δx = dy12 (每像素增加dy12)
// 垂直增量: Δλ₀/Δy = dx12 (每行增加dx12)
//
// 伪代码:
// 初始化: λ₀ = compute_initial(x_start, y_start)
// 水平扫描: λ₀ += dy12 (每向右移1像素)
// 换行: λ₀ += dx12 (每向下移1行,同时x重置)
//
// 同理: λ₁增量 = dy20(水平), dx20(垂直)
增量式方法将每个片段的计算从3次乘法+2次加法降为3次加法,性能提升约5倍。
现代GPU普遍采用分层光栅化,先做粗粒度测试再做细粒度:
将三角形的3条边向外扩展8像素,形成保守包围盒。Tile的4个角点如果都在3条边的内侧,则该Tile与三角形相交。
// 保守光栅化边测试
// 对每条边(ax+by+c>=0),计算tile 4个角的符号
// 如果4个角都满足 >= 0,则tile完全在内
// 如果4个角都不满足,则tile完全在外
// 否则tile部分相交,需逐像素检查
除了重心坐标插值,GPU还支持透视正确插值:
透视正确插值保证了纹理在透视变换下不产生畸变,这是GPU渲染管线的关键特性之一。
练习3:保守光栅化
实现保守光栅化:只要像素与三角形有任何重叠就生成片段。提示:将边方程的测试条件从">=0"改为">-0.5"。
练习4:8×8 Tile光栅化
实现分层光栅化:先做Tile级别的粗测试,再对相交Tile逐像素测试。评估性能提升。