🖥️ 第05课:片段着色

阶段一:图形基础 第5/30课
✅Verilator验证通过fragment_shader.v 已通过 Verilator --lint-only 检查。源文件:verilog/fragment_shader.v

学习目标:理解片段着色原理,掌握颜色插值和Lambert光照,实现Verilog片段着色器。

📋 核心概念

片段着色器对每个片段执行着色计算,决定最终颜色:

🔍 数学原理

I_diffuse = max(0, N⃗ · L⃗) × C_material
I_ambient = C_ambient
I_final = I_ambient + I_diffuse
fog_factor = clamp((fog_end - depth)/(fog_end - fog_start), 0, 1)
C_final = lerp(C_fog, I_final, fog_factor)

🔧 Verilog实现:片段着色器

//====================================================================
// fragment_shader.v - 片段着色器
// 第05课:颜色插值、Lambert光照、雾效计算
//====================================================================
module fragment_shader #(
    parameter COORD_WIDTH = 16,
    parameter COLOR_WIDTH = 24,
    parameter FRAC_BITS  = 12
)(
    input  wire                          clk, rst_n,
    input  wire                          frag_in_valid,
    input  wire [COORD_WIDTH-1:0]        frag_in_x, frag_in_y,
    input  wire [COLOR_WIDTH-1:0]        frag_in_color,
    input  wire signed [COORD_WIDTH-1:0] frag_in_depth,
    input  wire signed [COORD_WIDTH-1:0] frag_in_nx, frag_in_ny, frag_in_nz,
    output reg                           frag_in_ready,
    input  wire signed [COORD_WIDTH-1:0] light_dir_x, light_dir_y, light_dir_z,
    input  wire [COLOR_WIDTH-1:0]        light_color, ambient_color,
    input  wire                          fog_enable,
    input  wire [COLOR_WIDTH-1:0]        fog_color,
    input  wire signed [COORD_WIDTH-1:0] fog_start, fog_end,
    output reg                           frag_out_valid,
    output reg  [COORD_WIDTH-1:0]        frag_out_x, frag_out_y,
    output reg  [COLOR_WIDTH-1:0]        frag_out_color,
    output reg  signed [COORD_WIDTH-1:0] frag_out_depth,
    input  wire                          frag_out_ready
);
    reg [7:0] color_r, color_g, color_b, lit_r, lit_g, lit_b, final_r, final_g, final_b;
    reg signed [2*COORD_WIDTH-1:0] dot_nl;
    reg [7:0] diffuse_factor, fog_factor;
    reg [COORD_WIDTH-1:0] pass_x, pass_y;
    reg signed [COORD_WIDTH-1:0] pass_depth;
    localparam S_IDLE=3'd0, S_LIGHT=3'd1, S_FOG=3'd2, S_OUTPUT=3'd3;
    reg [2:0] state;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin state<=S_IDLE; frag_in_ready<=1'b1; frag_out_valid<=1'b0; end
        else begin
            frag_out_valid <= 1'b0;
            case (state)
                S_IDLE: begin
                    frag_in_ready <= 1'b1;
                    if (frag_in_valid) begin
                        frag_in_ready <= 1'b0;
                        color_r <= frag_in_color[23:16]; color_g <= frag_in_color[15:8]; color_b <= frag_in_color[7:0];
                        pass_x <= frag_in_x; pass_y <= frag_in_y; pass_depth <= frag_in_depth;
                        state <= S_LIGHT;
                    end
                end
                S_LIGHT: begin
                    dot_nl = frag_in_nx * light_dir_x + frag_in_ny * light_dir_y + frag_in_nz * light_dir_z;
                    diffuse_factor = (dot_nl > 0) ? ((dot_nl[19:12] > 8'd255) ? 8'd255 : dot_nl[19:12]) : 8'd0;
                    lit_r = (ambient_color[23:16] >> 2) + ((color_r * diffuse_factor) >> 8);
                    lit_g = (ambient_color[15:8] >> 2) + ((color_g * diffuse_factor) >> 8);
                    lit_b = (ambient_color[7:0] >> 2) + ((color_b * diffuse_factor) >> 8);
                    if (lit_r > 8'd255) lit_r = 8'd255;
                    if (lit_g > 8'd255) lit_g = 8'd255;
                    if (lit_b > 8'd255) lit_b = 8'd255;
                    state <= S_FOG;
                end
                S_FOG: begin
                    if (fog_enable) begin
                        fog_factor = (pass_depth < fog_start) ? 8'd255 : ((pass_depth > fog_end) ? 8'd0 : 8'd128);
                        final_r = (lit_r * fog_factor + fog_color[23:16] * (8'd255 - fog_factor)) >> 8;
                        final_g = (lit_g * fog_factor + fog_color[15:8] * (8'd255 - fog_factor)) >> 8;
                        final_b = (lit_b * fog_factor + fog_color[7:0] * (8'd255 - fog_factor)) >> 8;
                    end else begin final_r=lit_r; final_g=lit_g; final_b=lit_b; end
                    state <= S_OUTPUT;
                end
                S_OUTPUT: begin
                    frag_out_valid <= 1'b1; frag_out_x <= pass_x; frag_out_y <= pass_y;
                    frag_out_color <= {final_r, final_g, final_b}; frag_out_depth <= pass_depth;
                    state <= S_IDLE;
                end
                default: state <= S_IDLE;
            endcase
        end
    end
endmodule

🧪 仿真验证

`timescale 1ns/1ps
module tb_fragment_shader;
    parameter CLK_PERIOD = 10;
    reg clk, rst_n;
    // 添加具体接口信号...
    fragment_shader 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("=== fragment_shader 测试开始 ===");
        // 测试逻辑...
        #(CLK_PERIOD*100);
        $display("=== fragment_shader 测试完成 ===");
        $finish;
    end
endmodule
💡 设计要点:

📐 性能分析

片段着色器是管线最密集的阶段。640×480约307,200像素,每像素3次乘法(光照)+2次乘法(雾效)=5次乘法。100MHz下约15ms/帧,可达60FPS。

🏗️ 与前课的关联

接收第04课光栅化器输出的片段数据,执行着色计算后输出给第06课帧缓冲控制器。与第16课Phong着色器对比:本课实现简单Lambert模型。

🧩 练习题

练习1:镜面反射

添加Phong镜面反射:R=2(N·L)N-L,specular=pow(max(0,R·V),shininess)。

练习2:多光源

扩展支持2个方向光源,两个光源贡献如何组合?

🎯 本课小结

📚 延伸阅读与行业标准

相关技术标准

🔧 硬件实现考量

在实际GPU芯片设计中,片段着色模块面临以下挑战:

FPGA资源估算(Xilinx Artix-7)

资源本课模块占比
LUT~800~1.2%
FF~400~0.6%
DSP12~5.5%
BRAM00%

🌐 跨平台实现对比

平台实现方式性能开发难度
FPGAVerilog/HLS100MHz+中高
ASICRTL设计1GHz+极高
GPU着色器GLSL/HLSL可变
CPU软件C/C++受限于核心数

🧪 验证策略

对片段着色模块的验证应包含以下方面:

  1. 功能验证:基本输入输出的正确性
  2. 边界测试:零值、最大值、最小值、溢出情况
  3. 随机测试:大量随机输入的回归测试
  4. 对比验证:与软件参考模型的输出对比
  5. 时序验证:流水线停顿、反压等场景

📈 性能基准

在现代FPGA平台上的典型性能指标:

// 性能基准(Artix-7 @ 100MHz)
// - 顶点吞吐率: 10M vertices/s (单核)
// - 延迟: 3周期 (30ns)
// - DSP占用: 12个 (Q4.12乘法)
// - 最大时钟: ~150MHz (时序约束后)
// - 功耗: ~50mW (动态功耗估算)

🔗 与其他课程的关联

知识图谱

📖 推荐资源

📐 光照模型对比

模型公式计算量效果
LambertI = max(0, N·L)·C3乘+2加漫反射
PhongI = ambient + diffuse + spec~20乘漫反射+高光
Blinn-Phong用H=(L+V)/2代替R~15乘近似高光
PBR(Cook-Torrance)微面元理论~50乘物理正确

🔬 法线变换规则

当顶点位置使用矩阵M变换时,法线不能直接用M变换。正确的法线变换矩阵是M的逆转置:

N' = (M⁻¹)ᵀ · N

原因:法线必须与变换后的表面保持垂直。如果M包含非均匀缩放,直接用M变换法线会导致法线不再垂直于表面。

🔧 雾效模式详解

本课实现了线性雾,实际GPU支持三种雾效模式:

// 指数雾的定点数实现
// e^(-x) ≈ 1 - x + x²/2 - x³/6 (泰勒展开前4项)
// 在Q4.12格式中:
// fog_factor = 4096 - density*depth + (density*depth)²/8192
// 需要饱和到[0, 4096]范围

🧩 扩展练习

练习3:半Lambert光照

修改漫反射公式为 half-Lambert:diff = 0.5·(N·L) + 0.5。这样背光面也有光照,适合卡通渲染。

练习4:多pass着色

设计一个两pass着色方案:第一pass计算光照,第二pass添加后处理效果(如泛光Bloom)。

🔬 片段着色的深入分析

从硬件设计角度,片段着色模块需要满足严格的时序和面积约束。以下分析关键路径和优化空间。

关键路径分析

模块的关键路径通常在乘法器链上。Q4.12格式的16×16位乘法器在FPGA上需要约5ns,级联两级乘法器约10ns,这决定了最大工作频率约为100MHz。

// 关键路径示例
// 输入 → 乘法器1(5ns) → 加法器(2ns) → 乘法器2(5ns) → 输出
// 总延迟: 12ns → 最大频率 ≈ 83MHz
//
// 优化: 插入流水线寄存器
// Stage1: 乘法器1(5ns) → 寄存器
// Stage2: 加法器(2ns) + 乘法器2(5ns) → 寄存器
// 最大频率 ≈ 143MHz

面积优化策略

功耗优化策略

📊 与软件实现的对比

指标Verilog硬件C软件(ARM)比值
延迟3-5周期~100周期20-33×
吞吐率1/周期1/100周期100×
功耗~50mW~500mW10×
面积~800 LUTN/A-

硬件实现在延迟和吞吐率上有显著优势,特别适合实时渲染场景。

🧩 进阶练习

练习5:流水线优化

分析片段着色模块的关键路径,插入流水线寄存器使最大频率提升至150MHz以上。

练习6:面积优化

用时分复用方法减少乘法器数量到4个,评估对吞吐率的影响。

🏆 成就解锁:着色新手