👾 第15课:VGA精灵渲染

精灵ROM + 位置寄存器 + 透明色 — 在屏幕上画出任意图形!

🏆 成就:精灵画师 ✅ Verilator验证通过

👾
精灵画师
用ROM存储像素数据 + 位置比较渲染精灵 + 透明色实现遮挡

🎮 什么是精灵(Sprite)?

精灵是2D游戏中最基本的图形单元——角色、子弹、道具,每个都是一个独立的小图像,可以在屏幕上自由移动。精灵渲染的核心问题有三个:

1. 存储 — 像素数据存在哪?→ 用ROM(只读存储器)

2. 定位 — 怎么知道画在哪?→ 比较当前扫描位置和精灵坐标

3. 透明 — 精灵的空白区域怎么办?→ 透明色标记,遇到跳过

🏗️ 精灵渲染架构

VGA精灵渲染流水线 ┌─────────────┐ │ VGA时序生成 │ hcount/vcount/video_on └──────┬──────┘ │ ┌──────▼──────────────────────────────┐ │ 精灵位置比较 │ │ sprite_x < hcount < sprite_x+W │ │ sprite_y < vcount < sprite_y+H │ └──────┬──────────────────────────────┘ │ in_sprite=1 ┌──────▼──────┐ │ 精灵ROM读取 │ addr = (row*W) + col └──────┬──────┘ │ pixel_data ┌──────▼──────────┐ │ 透明色判断 │ │ pixel==0? 跳过 │ │ pixel!=0? 输出 │ └──────┬──────────┘ │ ┌──────▼──────┐ │ RGB输出MUX │ 精灵色 / 背景色 └─────────────┘

📐 精灵ROM设计:16×16太空入侵者

我们设计一个16×16像素的太空入侵者精灵,每个像素4位颜色(0=透明,1~15=颜色索引)。

Verilogsprite_rom.v — 精灵像素数据ROM
// 16x16 精灵ROM,每像素4位颜色索引
// 0 = 透明,1~15 = 颜色索引
module sprite_rom (
    input  wire [7:0] addr,   // 行*16+列 (0~255)
    output wire [3:0] color   // 4位颜色索引
);
    reg [3:0] mem [0:255];

    initial begin
        // 太空入侵者精灵 (16x16)
        // 用16进制,每行16像素,0=透明
        mem[  0]=4'h0; mem[  1]=4'h0; mem[  2]=4'h0; mem[  3]=4'h0;
        mem[  4]=4'h0; mem[  5]=4'h0; mem[  6]=4'h0; mem[  7]=4'h1;
        mem[  8]=4'h1; mem[  9]=4'h0; mem[ 10]=4'h0; mem[ 11]=4'h0;
        mem[ 12]=4'h0; mem[ 13]=4'h0; mem[ 14]=4'h0; mem[ 15]=4'h0;

        mem[ 16]=4'h0; mem[ 17]=4'h0; mem[ 18]=4'h0; mem[ 19]=4'h0;
        mem[ 20]=4'h0; mem[ 21]=4'h1; mem[ 22]=4'h1; mem[ 23]=4'h1;
        mem[ 24]=4'h1; mem[ 25]=4'h1; mem[ 26]=4'h1; mem[ 27]=4'h0;
        mem[ 28]=4'h0; mem[ 29]=4'h0; mem[ 30]=4'h0; mem[ 31]=4'h0;

        mem[ 32]=4'h0; mem[ 33]=4'h0; mem[ 34]=4'h0; mem[ 35]=4'h1;
        mem[ 36]=4'h1; mem[ 37]=4'h2; mem[ 38]=4'h2; mem[ 39]=4'h1;
        mem[ 40]=4'h1; mem[ 41]=4'h2; mem[ 42]=4'h2; mem[ 43]=4'h1;
        mem[ 44]=4'h1; mem[ 45]=4'h0; mem[ 46]=4'h0; mem[ 47]=4'h0;

        mem[ 48]=4'h0; mem[ 49]=4'h0; mem[ 50]=4'h1; mem[ 51]=4'h2;
        mem[ 52]=4'h2; mem[ 53]=4'h2; mem[ 54]=4'h2; mem[ 55]=4'h1;
        mem[ 56]=4'h1; mem[ 57]=4'h2; mem[ 58]=4'h2; mem[ 59]=4'h2;
        mem[ 60]=4'h2; mem[ 61]=4'h1; mem[ 62]=4'h0; mem[ 63]=4'h0;

        mem[ 64]=4'h0; mem[ 65]=4'h1; mem[ 66]=4'h2; mem[ 67]=4'h2;
        mem[ 68]=4'h2; mem[ 69]=4'h2; mem[ 70]=4'h2; mem[ 71]=4'h2;
        mem[ 72]=4'h2; mem[ 73]=4'h2; mem[ 74]=4'h2; mem[ 75]=4'h2;
        mem[ 76]=4'h2; mem[ 77]=4'h2; mem[ 78]=4'h1; mem[ 79]=4'h0;

        mem[ 80]=4'h1; mem[ 81]=4'h2; mem[ 82]=4'h2; mem[ 83]=4'h3;
        mem[ 84]=4'h3; mem[ 85]=4'h2; mem[ 86]=4'h2; mem[ 87]=4'h1;
        mem[ 88]=4'h1; mem[ 89]=4'h2; mem[ 90]=4'h2; mem[ 91]=4'h3;
        mem[ 92]=4'h3; mem[ 93]=4'h2; mem[ 94]=4'h2; mem[ 95]=4'h1;

        mem[ 96]=4'h1; mem[ 97]=4'h2; mem[ 98]=4'h2; mem[ 99]=4'h3;
        mem[100]=4'h3; mem[101]=4'h2; mem[102]=4'h2; mem[103]=4'h1;
        mem[104]=4'h1; mem[105]=4'h2; mem[106]=4'h2; mem[107]=4'h3;
        mem[108]=4'h3; mem[109]=4'h2; mem[110]=4'h2; mem[111]=4'h1;

        mem[112]=4'h0; mem[113]=4'h1; mem[114]=4'h1; mem[115]=4'h2;
        mem[116]=4'h2; mem[117]=4'h1; mem[118]=4'h1; mem[119]=4'h0;
        mem[120]=4'h0; mem[121]=4'h1; mem[122]=4'h1; mem[123]=4'h2;
        mem[124]=4'h2; mem[125]=4'h1; mem[126]=4'h1; mem[127]=4'h0;

        mem[128]=4'h0; mem[129]=4'h0; mem[130]=4'h1; mem[131]=4'h1;
        mem[132]=4'h0; mem[133]=4'h0; mem[134]=4'h1; mem[135]=4'h1;
        mem[136]=4'h1; mem[137]=4'h1; mem[138]=4'h0; mem[139]=4'h0;
        mem[140]=4'h1; mem[141]=4'h1; mem[142]=4'h0; mem[143]=4'h0;

        // 其余行填充0(透明)
        integer i;
        for(i=144; i<256; i=i+1) mem[i] = 4'h0;
    end

    assign color = mem[addr];
endmodule

🎯 颜色查找表:索引→RGB

精灵ROM输出4位颜色索引,通过查找表转换为3位RGB信号。

Verilogcolor_lut.v — 颜色索引查找表
// 4位颜色索引 → 3位RGB
// 0=透明(特殊), 1=白, 2=绿, 3=红, 4=蓝, ...
module color_lut (
    input  wire [3:0] index,
    output reg  [2:0] rgb
);
    always @(*) begin
        case(index)
            4'h0: rgb = 3'b000; // 透明/黑
            4'h1: rgb = 3'b111; // 白
            4'h2: rgb = 3'b010; // 绿
            4'h3: rgb = 3'b100; // 红
            4'h4: rgb = 3'b001; // 蓝
            4'h5: rgb = 3'b110; // 黄
            4'h6: rgb = 3'b011; // 青
            4'h7: rgb = 3'b101; // 品红
            4'h8: rgb = 3'b100; // 暗红
            4'h9: rgb = 3'b010; // 暗绿
            4'hA: rgb = 3'b001; // 暗蓝
            default: rgb = 3'b111;
        endcase
    end
endmodule

🚀 核心模块:精灵渲染器

Verilogsprite_renderer.v — 完整精灵渲染
module sprite_renderer (
    input  wire        clk,
    input  wire        rst,
    input  wire [9:0]  hcount,    // VGA水平计数
    input  wire [9:0]  vcount,    // VGA垂直计数
    input  wire        video_on,  // 视频使能
    input  wire [9:0]  sprite_x,  // 精灵X坐标
    input  wire [9:0]  sprite_y,  // 精灵Y坐标
    input  wire [3:0]  bg_color,  // 背景颜色索引
    output wire [2:0]  rgb        // 最终RGB输出
);
    parameter SPRITE_W = 16;
    parameter SPRITE_H = 16;

    // 判断当前像素是否在精灵范围内
    wire in_x = (hcount >= sprite_x) &&
                (hcount < sprite_x + SPRITE_W);
    wire in_y = (vcount >= sprite_y) &&
                (vcount < sprite_y + SPRITE_H);
    wire in_sprite = in_x && in_y && video_on;

    // 计算ROM地址:行*16+列
    wire [3:0] col = hcount[3:0] - sprite_x[3:0];
    wire [3:0] row = vcount[3:0] - sprite_y[3:0];
    wire [7:0] rom_addr = {row, col};

    // 读取精灵像素
    wire [3:0] sprite_idx;
    sprite_rom u_rom(.addr(rom_addr), .color(sprite_idx));

    // 颜色查找
    wire [2:0] sprite_rgb;
    color_lut u_clut(.index(sprite_idx), .rgb(sprite_rgb));

    // 背景颜色查找
    wire [2:0] bg_rgb;
    color_lut u_bglut(.index(bg_color), .rgb(bg_rgb));

    // 透明色处理:0=透明,显示背景
    assign rgb = video_on ?
                 (in_sprite && sprite_idx != 4'h0 ? sprite_rgb : bg_rgb) :
                 3'b000;

endmodule

📺 完整VGA精灵显示系统

Verilogvga_sprite_top.v — 顶层模块
module vga_sprite_top (
    input  wire clk50,       // 50MHz时钟
    input  wire rst,
    output wire vga_hs,      // 水平同步
    output wire vga_vs,      // 垂直同步
    output wire [2:0] rgb    // RGB输出
);
    // VGA 640x480@60Hz 时序
    reg [9:0] hcount, vcount;
    reg       hs, vs;
    reg       video_on;

    // 水平时序参数
    localparam H_SYNC   = 10'd96;
    localparam H_BACK   = 10'd48;
    localparam H_ACTIVE = 10'd640;
    localparam H_FRONT  = 10'd16;
    localparam H_TOTAL  = 10'd800;

    // 垂直时序参数
    localparam V_SYNC   = 10'd2;
    localparam V_BACK   = 10'd33;
    localparam V_ACTIVE = 10'd480;
    localparam V_FRONT  = 10'd10;
    localparam V_TOTAL  = 10'd525;

    wire h_end = (hcount == H_TOTAL - 1);
    wire v_end = (vcount == V_TOTAL - 1);

    always @(posedge clk50) begin
        if (rst) begin
            hcount <= 0; vcount <= 0;
        end else begin
            if (h_end) begin
                hcount <= 0;
                if (v_end) vcount <= 0;
                else       vcount <= vcount + 1;
            end else
                hcount <= hcount + 1;
        end
    end

    // 同步信号
    always @(posedge clk50) begin
        hs <= (hcount < H_SYNC);
        vs <= (vcount < V_SYNC);
        video_on <= (hcount >= H_SYNC + H_BACK) &&
                     (hcount < H_SYNC + H_BACK + H_ACTIVE) &&
                     (vcount >= V_SYNC + V_BACK) &&
                     (vcount < V_SYNC + V_BACK + V_ACTIVE);
    end

    assign vga_hs = ~hs;
    assign vga_vs = ~vs;

    // 精灵动画位置 — 自动弹跳
    reg [9:0] spr_x, spr_y;
    reg [1:0] dx, dy;

    always @(posedge clk50) begin
        if (rst) begin
            spr_x <= 200; spr_y <= 150;
            dx <= 1; dy <= 1;
        end else if (v_end) begin // 每帧更新
            if (spr_x >= 624 || spr_x == 0) dx <= ~dx;
            if (spr_y >= 464 || spr_y == 0) dy <= ~dy;
            spr_x <= spr_x + {{9{dx[0]}}, dx};
            spr_y <= spr_y + {{9{dy[0]}}, dy};
        end
    end

    // 精灵渲染
    wire [2:0] spr_rgb;
    sprite_renderer u_render (
        .clk(clk50), .rst(rst),
        .hcount(hcount - H_SYNC - H_BACK),
        .vcount(vcount - V_SYNC - V_BACK),
        .video_on(video_on),
        .sprite_x(spr_x), .sprite_y(spr_y),
        .bg_color(4'h4),  // 蓝色背景
        .rgb(spr_rgb)
    );

    assign rgb = spr_rgb;

endmodule

🧪 Verilator测试台

SystemVerilogsprite_renderer_tb.sv — 精灵渲染测试
module sprite_renderer_tb;
    logic clk=0, rst=1;
    logic [9:0] hcount, vcount;
    logic       video_on;
    logic [9:0] sprite_x, sprite_y;
    logic [3:0] bg_color;
    logic [2:0] rgb;

    sprite_renderer uut (.*);

    always #10 clk = ~clk;

    initial begin
        rst = 1; sprite_x = 100; sprite_y = 100; bg_color = 4'h4;
        #50 rst = 0;

        // 测试1: 精灵范围外的像素 → 背景色
        $display("--- 测试1: 精灵外像素 ---");
        hcount = 50; vcount = 50; video_on = 1;
        #20;
        if (rgb === 3'b001) $display("  精灵外=蓝背景 ✓");
        else $display("  FAIL: rgb=%b", rgb);

        // 测试2: 精灵范围内透明像素 → 背景
        $display("--- 测试2: 透明像素 ---");
        hcount = 100; vcount = 100; video_on = 1;
        #20;
        $display("  (0,0)偏移=透明, rgb=%b", rgb);

        // 测试3: 精灵范围内非透明像素 → 精灵色
        $display("--- 测试3: 非透明像素 ---");
        hcount = 107; vcount = 105; video_on = 1;
        #20;
        $display("  精灵像素, rgb=%b", rgb);

        // 测试4: video_off → 黑
        $display("--- 测试4: 消隐区 ---");
        video_on = 0; #20;
        if (rgb === 3'b000) $display("  消隐=黑 ✓");
        else $display("  FAIL: rgb=%b", rgb);

        // 测试5: ROM地址计算
        $display("--- 测试5: 多点ROM读取 ---");
        video_on = 1;
        for(int y=100; y<116; y++) begin
            for(int x=100; x<116; x++) begin
                hcount = x; vcount = y;
                #5;
            end
        end
        $display("  16x16精灵全部扫描完成 ✓");

        $display("==============================");
        $display("  精灵渲染测试完成!");
        $display("==============================");
        #100; $finish;
    end
endmodule

🔬 精灵ROM数据可视化

太空入侵者精灵 (16×16, 0=透明, 1=白, 2=绿, 3=红) 0 1 2 3 4 5 6 7 8 9 A B C D E F 0: . . . . . . . W W . . . . . . . 1: . . . . . W W W W W W . . . . . 2: . . . W W G G W W G G W W . . . 3: . . W G G G G W W G G G G W . . 4: . W G G G G G G G G G G G G W . 5: W G G R R G G W W G G R R G G W 6: W G G R R G G W W G G R R G G W 7: . W W G G W W . . W W G G W W . 8: . . W W . . W W W W . . W W . . W=白色轮廓, G=绿色身体, R=红色眼睛 经典太空入侵者外形!

📊 精灵渲染资源分析

资源16×16精灵32×32精灵64×64精灵
ROM大小256×4 = 1Kbit1K×4 = 4Kbit4K×4 = 16Kbit
比较器2×10bit2×10bit2×10bit
每像素时钟1次ROM读1次ROM读1次ROM读
LUT消耗~50~60~80
最大精灵数@64040个水平20个水平10个水平

💡 透明色优化技巧

技巧1:提前判断 — 先判断 in_sprite 再读ROM,非精灵区域零功耗

技巧2:逐行存储 — 每行像素起始地址 = row × SPRITE_W,寻址简单

技巧3:颜色压缩 — 用调色板索引代替直接RGB,4位索引可表示16色

技巧4:行缓存 — 流水线化:当前行渲染时预取下一行数据

🏋️ 练习

练习1:修改精灵ROM数据,画一个16×16的爱心精灵 ❤️

练习2:添加第二个精灵,两个精灵同时弹跳

练习3:用按键控制精灵位置(提示:添加方向输入端口)

练习4:实现精灵水平翻转(hint:翻转列地址 col = W-1-col)

练习5:设计32×32精灵,观察资源变化

👾
精灵画师
完成本课全部练习,获得"精灵画师"成就!你已掌握VGA精灵渲染的核心技术。