你有没有想过:一个只存 truefalse 的列表,竟然能压缩到原来的 1/8 大小?
本文教你用一个超简单的技巧,让大型 3D 场景、图像处理、动画控制等数据传输快 8 倍,内存占用直降 87.5%!


🎯 问题:我们为什么需要压缩布尔值?

想象你在做一个 3D 渲染引擎,用 Rust 编写后端逻辑,通过 WebAssembly(WASM)传给前端的 Three.js。

你有一个场景,里面有 200 万个物体
你想告诉前端:“哪些物体现在在相机视野里?”

最直接的做法是:

let visible: Vec<bool> = vec![true, false, true, false, ...]; // 200万个元素

每个 truefalse 在内存里占 1个字节 → 总共 2,000,000 字节 = 2MB!

💡 2MB 传一次,如果每帧都传(60帧/秒),那就是 120MB/秒!浏览器会卡顿,手机直接炸!

我们能不能只传“信息”本身,而不是“每个信息都用一个字节来写”?

答案是:能!我们用一个字节,存 8 个开关!


🧠 比喻:就像用一个电灯开关面板,控制8盏灯

想象你家有一个8孔插座面板,每个孔插一个灯泡开关:

[孔1][孔2][孔3][孔4][孔5][孔6][孔7][孔8]
   ↓   ↓   ↓   ↓   ↓   ↓   ↓   ↓
  开   关   开   关   开   关   开   关

你可以用 1个字节(8位)记录这 8 盏灯的开关状态:

  • 1 = 开灯(true)
  • 0 = 关灯(false)

那这个面板的状态就是:

二进制:1 0 1 0 1 0 1 0
十进制:170(就是数字 170)

你只需要告诉别人:“面板是 170”,对方就知道哪盏灯亮、哪盏灯灭!

这就是我们今天要实现的魔法:用一个数字,记录8个布尔值!


✅ 技术实现:两个函数搞定

我们用 Rust 编写两个函数:

1️⃣ bool_to_bitmap:把布尔数组 → 压缩成字节

use std::u8;
/// 将布尔值切片转换为位图(每个字节存储8个布尔值)
/// 
/// # 参数
/// * `bools` - 布尔值切片引用
/// 
/// # 返回值
/// * `Vec<u8>` - 包含位图数据的字节向量
fn bool_to_bitmap(bools: &[bool]) -> Vec<u8> {
    // 存储最终位图数据的向量
    let mut bitmap:Vec<u8> = Vec::new();
    // 当前正在构建的字节,初始化为0
    let mut current_byte:u8 = 0u8;
    // 当前在字节中的位计数器(0-7)
    let mut bit_count:u8 = 0;
    
    // 遍历输入的布尔值切片
    for &b in bools{
        // 如果当前布尔值为true,则需要将对应位设置为1
        if b{
           // 位操作解释:
            // 1 << bit_count: 将数字1向左移动bit_count位
            // 位编号从右到左,最右边是第0位(与数组索引一样从0开始)
            // 例如:当bit_count=0时,1<<0=1 (二进制: 00000001) - 第0位(最右边)被设置为1
            //      当bit_count=1时,1<<1=2 (二进制: 00000010) - 第1位被设置为1
            //      当bit_count=2时,1<<2=4 (二进制: 00000100) - 第2位被设置为1
            //      ...
            //      当bit_count=7时,1<<7=128 (二进制: 10000000) - 第7位(最左边)被设置为1
            // |= : 按位或赋值操作符,将current_byte(左操作数)与1 << bit_count(右操作数)进行按位或运算,并将结果赋值给current_byte
            // 按位或的作用:只将特定位设置为1,而不影响其他位
            // 例如:
            // 1. 假设current_byte是00000000,bit_count=2
            //    1 << 2 = 00000100
            //    00000000 | 00000100 = 00000100
            //    结果:第2位被设置为1
            // 2. 现在current_byte是00000100,bit_count=0
            //    1 << 0 = 00000001
            //    00000100 | 00000001 = 00000101
            //    结果:第0位也被设置为1,第2位保持不变
            // 3. 现在current_byte是00000101,bit_count=1
            //    1 << 1 = 00000010
            //    00000101 | 00000010 = 00000111
            //    结果:第1位也被设置为1,第0位和第2位保持不变
            current_byte |= 1 << bit_count;
        }
        // 位计数器递增
        bit_count += 1;
        // 如果已经处理了8个位(一个完整的字节)
        if bit_count == 8{
            // 将构建好的字节添加到位图向量中
            bitmap.push(current_byte);
            // 重置当前字节和位计数器,开始构建下一个字节
            current_byte = 0;
            bit_count = 0;
        }
    }
    // 如果还有未完成的字节(即输入的布尔值数量不是8的倍数)
    if bit_count > 0{
        // 将最后一个不完整的字节添加到位图向量中
        bitmap.push(current_byte);
    }
    // 返回位图数据
    bitmap
}

2️⃣ bitmap_to_bools:把压缩后的字节 → 还原成布尔数组

/// 将位图转换回布尔值向量
/// 
/// # 参数
/// * `bitmap` - 位图数据(字节向量)
/// * `bool_count` - 原始布尔值的数量(因为最后一位字节可能包含填充位)
/// 
/// # 返回值
/// * `Vec<bool>` - 布尔值向量
fn bitmap_to_bools(bitmap: &[u8], bool_count: usize) -> Vec<bool> {
    let mut bools:Vec<bool> = Vec::with_capacity(bool_count);
    
    // 遍位列图中的每个字节
    for &byte in bitmap {
        // 对于每个字节,检查其中的每一位(共8位)
        for i in 0..8 {
            // 如果已达到所需的布尔值数量,则停止
            if bools.len() >= bool_count {
                break;
            }
            
            // 检查第i位是否为1
            // 使用按位与操作符(&)和右移操作符(>>)
            // (byte >> i) & 1 的工作原理(通过具体例子说明):
            // 假设 byte = 5,二进制表示为 00000101
            // 我们想检查第1位(从右边数起,从0开始编号)是否为1
            // 
            // 步骤1: byte >> i
            //   byte >> 1 表示将 00000101 右移1位
            //   结果是 00000010 (原来的第1位现在变成了第0位)
            //
            // 步骤2: & 1
            //   00000010 & 00000001 = 00000000
            //   因为按位与运算规则:只有两个位都是1时,结果才是1
            //   其他位都与0进行按位与运算,所以都变成0
            //   只有最右边的位参与运算,得到0
            //
            // 所以结果是0,表示原byte的第1位是0(false)
            // 
            // 再举一个例子,检查第0位:
            // 步骤1: byte >> 0 (不移动)
            //   00000101 >> 0 = 00000101
            // 步骤2: & 1
            //   00000101 & 00000001 = 00000001
            //   
            // 所以结果是1,表示原byte的第0位是1(true)
            let bit_value = (byte >> i) & 1;
            bools.push(bit_value == 1);
        }
    }    
    bools
}

🧪 举个例子:实际跑一遍

fn main() {
    let lights = &[true, false, true, false, true, false, true, false]; // 8个开关

    let compressed = bool_to_bitmap(lights); // 压缩!
    println!("压缩后:{:08b}", compressed[0]); // 输出:**01010101**(二进制)
    println!("压缩后字节值:{}", compressed[0]); // 输出:**85**(十进制)✅
}

输出:

压缩后:01010101
压缩后字节值:85
还原后:[true, false, true, false, true, false, true, false]

✅ 完全一致!但我们只用了 1 个字节,而不是 8 个字节!


🚀 应用场景:你能在哪些地方用上它?

场景 原始数据量 压缩后 压缩比 价值
3D 场景中 200 万个物体是否可见? 2,000,000 字节(2MB) 250,000 字节(244KB) 8倍 ✅ 减少 87.5% 传输量!
图像处理中,哪些像素要加滤镜? 4K 图像 = 8,294,400 像素 ~1MB 8倍 ✅ GPU 可直接读取为纹理
动画中,哪些帧有关键动作? 1000 帧 125 字节 8倍 ✅ 节省存储空间
用户在画布上选中了哪些区域? 100万像素选区 125KB 8倍 ✅ 实时交互更流畅
哪些物体参与了物理碰撞? 5000 个物体 625 字节 8倍 ✅ 物理引擎更快

💡 特别适合:Rust + WASM + Three.js 架构
你可以在 Rust 里做复杂计算(比如视锥剔除、光照判断),把结果压缩成字节,传给前端 JS,前端用 Three.js 渲染,快到飞起!


🔧 如何在前端(JavaScript)使用?

WASM 传回的是 Uint8Array,你可以直接用:

// 假设从 WASM 拿到压缩数据
const compressedData = wasmModule.getVisibilityMask(); // Uint8Array

// 用 JavaScript 解压(可选,也可以在 WASM 里解压后返回)
function unpackBitmap(bitmap, count) {
    const result = [];
    for (let byte of bitmap) {
        for (let i = 0; i < 8; i++) {
            if (result.length >= count) break;
            result.push(!!(byte & (1 << i))); // 与运算取位,转为布尔值
        }
    }
    return result;
}

const visibleObjects = unpackBitmap(compressedData, 2000000);
visibleObjects.forEach((isVisible, index) => {
    if (isVisible) {
        scene.getObjectById(index).visible = true;
    } else {
        scene.getObjectById(index).visible = false;
    }
});

注意:你甚至可以不解压,直接把 Uint8Array 上传为 WebGL 纹理,在着色器里做判断,性能再翻倍!


📌 总结:一句话记住它

“1个字节,8个开关;省空间、快传输、低内存,前端性能起飞!”


✅ 为什么这招这么牛?

传统做法 位图压缩
每个 true/false 占 1 字节 每 8 个只占 1 字节
传输 2MB 数据 传输 244KB
内存占用高 内存占用极低
浏览器容易卡顿 流畅运行
没有通用性 可用于图像、动画、3D、AI 掩码

📚 扩展阅读(进阶玩法)

  • ✅ 把压缩后的位图上传为 WebGL 纹理,在 GPU 里直接做判断(无需传回 JS)
  • ✅ 用 wasm-opt 优化 WASM 文件体积(你已会用,太棒了!)
  • ✅ 用 rayon 并行计算布尔值,再压缩 → 极致性能
  • ✅ 支持 LUT 应用区域掩码:只对“选中区域”应用颜色校正,大幅提升效率

💬 最后一句

不是所有优化都要写复杂算法。
有时候,一个字节存8个开关,就是最优雅、最高效的解决方案。


📌 收藏这篇,下次做 3D 渲染、图像处理、动画系统时,记得用它!


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐