MATLAB二维翼型DFFD变形工具:一键读取翼型+网格,自动构建控制体并输出变形后壁面与块结构数据
2026/6/10 8:04:04
| 扩展名 | 格式名称 | 色深 | 是否有调色板 | 用途 |
|---|---|---|---|---|
.clut | CLUT | 24-bit | 自身就是调色板 | 256色 RGB 调色板 |
.rgb | RGB565 | 16-bit | 否 | 真彩色纹理 |
.rmap8 | RMAP8 | 8-bit | 需要 .clut | 8位索引纹理(地形/物体) |
.tmap8 | TMAP8 | 8-bit | 需要 .clut | 8位索引动画纹理 |
.tmap32 | TMAP32 | 32-bit | 否 | 带透明通道的真彩色纹理 |
.hmap | HMAP | 8-bit | 内置地形色 | 高度图/地形数据 |
.pal | PAL | 24-bit | 自身就是调色板组 | 多组调色板 |
.spr | SPR | 8-bit | 需要 .pal | 精灵/按钮/字体纹理 |
+--------+--------------------------------+ | 偏移 | 内容 | +--------+--------------------------------+ | 0 | R0 (红色分量,第0色) | | 1 | G0 (绿色分量,第0色) | | 2 | B0 (蓝色分量,第0色) | | 3 | R1 | | ... | ... | | 765 | R255 | | 766 | G255 | | 767 | B255 | +--------+--------------------------------+for (int i = 0; i < 256; ++i) { int r = data[i * 3 + 0]; int g = data[i * 3 + 1]; int b = data[i * 3 + 2]; palette[i] = RGB(r, g, b); }为.rmap8和.tmap8文件提供颜色查找表。一个纹理文件通常对应多个 CLUT 变体(见 CLUT 调色板类型)。
+--------+--------+--------------------------------+ | 偏移 | 大小 | 内容 | +--------+--------+--------------------------------+ | 0 | 8 | "RAW RGB " (ASCII 文件标识) | | 8 | 2 | Width (uint16, little-endian) | | 10 | 2 | Height (uint16, little-endian) | | 12 | 2 | Pixel[0] (RGB565) | | 14 | 2 | Pixel[1] | | ... | ... | ... | +--------+--------+--------------------------------+// 读取 16-bit 像素 uint16_t pixel = data[offset] | (data[offset + 1] << 8); // 解码 RGB565 int r5 = (pixel >> 11) & 0x1F; // 5-bit red int g6 = (pixel >> 5) & 0x3F; // 6-bit green int b5 = pixel & 0x1F; // 5-bit blue // 扩展到 8-bit int r = (r5 << 3) | (r5 >> 2); // 5->8 bit int g = (g6 << 2) | (g6 >> 4); // 6->8 bit int b = (b5 << 3) | (b5 >> 2); // 5->8 bit+--------+--------+--------------------------------+ | 偏移 | 大小 | 内容 | +--------+--------+--------------------------------+ | 0 | 128 | 文件头 (未知内容) | | 128 | 1 | Pixel[0] (8-bit 调色板索引) | | 129 | 1 | Pixel[1] | | ... | ... | ... | +--------+--------+--------------------------------+// 跳过 128 字节头部 QByteArray pixelData = data.mid(128); // 使用 CLUT 调色板渲染 QImage img(width, height, QImage::Format_Indexed8); img.setColorTable(clutPalette); memcpy(img.bits(), pixelData.constData(), width * height);与 RMAP8 相同:
+--------+--------+--------------------------------+ | 偏移 | 大小 | 内容 | +--------+--------+--------------------------------+ | 0 | 128 | 文件头 | | 128 | N | 像素数据 (8-bit 索引) | +--------+--------+--------------------------------+int totalPixels = pixelData.size(); int singleFramePixels = width * height; int frameCount = totalPixels / singleFramePixels; // 如果总像素是单帧的整数倍,则为动画 if (totalPixels % singleFramePixels == 0) { // 多帧动画 } // 或者:高度远大于宽度时,可能是竖直条带动画 if (height > width * 2 && height % width == 0) { frameCount = height / width; height = width; // 每帧为正方形 }for (int f = 0; f < frameCount; ++f) { QByteArray frameData = pixelData.mid(f * singleFramePixels, singleFramePixels); QImage frame = indexedToImage(frameData, width, height); }+--------+--------+--------------------------------+ | 偏移 | 大小 | 内容 | +--------+--------+--------------------------------+ | 0 | 1 | B0 (蓝色,第0像素) | | 1 | 1 | G0 (绿色) | | 2 | 1 | R0 (红色) | | 3 | 1 | A0 (Alpha,透明度) | | 4 | 1 | B1 | | ... | ... | ... | +--------+--------+--------------------------------+for (每个像素) { uint8_t b = data[offset + 0]; uint8_t g = data[offset + 1]; uint8_t r = data[offset + 2]; uint8_t a = data[offset + 3]; pixel = RGBA(r, g, b, a); }+--------+--------+--------------------------------+ | 偏移 | 大小 | 内容 | +--------+--------+--------------------------------+ | 0 | H | 文件头 (可变大小) | | H | 1128251| 高度数据 (1501×751 字节) | +--------+--------+--------------------------------+根据 Beachhead16.exe.lst 分析:
// sub_4259DA - 加载 HMap FILE* fp = fopen("HMap1501x751C.hmap", "rb"); char buffer[1128251]; // 读取并丢弃头部(通常为 128 字节) fread(buffer, 1, 128, fp); // 读取实际高度数据(覆盖头部缓冲区) fread(buffer, 1, 1128251, fp); fclose(fp); // 获取高度值 uint8_t height = buffer[pixelX + pixelY * 1501];| 高度值范围 | 地形类型 | 颜色 |
|---|---|---|
| 0-51 (0-20%) | 深水 | 深蓝 |
| 52-89 (21-35%) | 浅水 | 蓝色 |
| 90-102 (36-40%) | 沙滩 | 黄色 |
| 103-178 (41-70%) | 草地/陆地 | 绿色 |
| 179-216 (71-85%) | 山丘 | 棕色 |
| 217-255 (86-100%) | 雪地 | 白色 |
| 值 | 地形 |
|---|---|
| 0 | 空 |
| 1 | 地面 |
| 7 | 水 |
| 9 | 深水 |
| 0xA | 浅水 |
| 0xB | 沙滩 |
| 0xC | 雪地 |
| 0xD | 天空 |
+--------+--------+--------------------------------+ | 偏移 | 大小 | 内容 | +--------+--------+--------------------------------+ | 0 | 4 | Offset[0] (uint32, little-endian)| | 4 | 4 | Offset[1] | | ... | ... | ... | | 4N | 4 | Offset[N-1] | | Offset[0] | M0 | Palette[0] 数据 (RGB 三元组) | | Offset[1] | M1 | Palette[1] 数据 | | ... | ... | ... | +--------+--------+--------------------------------+struct SpriteHeader { uint8_t type; // 0x05 uint16_t width; // little-endian uint16_t height; // little-endian uint8_t palIndex; // PAL 调色板索引 uint8_t unknown[2]; // 未知 };// 1. 加载同名 PAL 文件(如 UNITS.SPR → UNITS.PAL) // 2. 对每个精灵: // - 读取头部获取 width/height/palIndex // - 从 PAL 中选择对应调色板 // - 读取像素数据(width × height 字节) // - 索引 0 为透明色 // - 其他索引通过调色板转换为 RGB| 文件名 | 精灵数 | 用途 |
|---|---|---|
| MENUS.SPR | 28 | 菜单元素 |
| INNAME.SPR | 4 | 姓名输入界面 |
| UNITS.SPR | 2 | 游戏单位(坦克等) |
| CONTROLS.SPR | 2 | 控件按钮 |
| LOGOS.SPR | 2 | Logo |
| ARIAL.SPR | 2 | 字体 |
根据 Beachhead16.exe.lst 逆向分析,游戏引擎支持以下 CLUT 变体:
| 后缀 | 完整示例 | 说明 |
|---|---|---|
| (无) | Ocean.clut | 基础调色板 |
GenAlpha | OceanGenAlpha.clut | 生成 Alpha 通道 |
GenAlpha2 | OceanGenAlpha2.clut | 生成 Alpha 通道(变体2) |
Expand | OceanExpand.clut | 扩展颜色范围 |
Expand555 | OceanExpand555.clut | 扩展到 RGB555 |
Shaded | OceanShaded.clut | 带阴影效果 |
QStringList clutTypes = { "", "GenAlpha", "GenAlpha2", "Expand", "Expand555", "Shaded" }; for (const QString &type : clutTypes) { QString clutPath = baseName + type + ".clut"; // 尝试加载... }如果自动匹配失败,可通过文件对话框手动选择 CLUT/PAL 文件。
.rmap8 / .tmap8 ──→ .clut (CLUT 调色板) .spr ──→ .pal (PAL 调色板组) .rgb ──→ (无,自带 RGB565 数据) .tmap32 ──→ (无,自带 BGRA 数据) .hmap ──→ (无,使用内置地形色) .clut ──→ (自身就是调色板) .pal ──→ (自身就是调色板组)// 读取偏移表 while (offset < fileSize) { uint32_t off = read_uint32_le(); if (off >= fileSize) break; offsets.push_back(off); } // 读取每个调色板 for (每个偏移) { int paletteSize = nextOffset - currentOffset; int colorCount = paletteSize / 3; for (int c = 0; c < colorCount; ++c) { int r = data[off + c * 3 + 0]; int g = data[off + c * 3 + 1]; int b = data[off + c * 3 + 2]; } }| 文件名 | 调色板数 | 每调色板大小 | 用途 |
|---|---|---|---|
| MENUS.PAL | 15 | 770 字节 | 菜单界面 |
| INNAME.PAL | 4 | 770 字节 | 姓名输入 |
| UNITS.PAL | 2 | 770 字节 | 游戏单位 |
| CONTROLS.PAL | 2 | 770 字节 | 控件按钮 |
| LOGOS.PAL | 2 | 770 字节 | Logo |
| ARIAL.PAL | 2 | ~383 字节 | 字体 |
+--------+--------+--------------------------------+ | 偏移 | 大小 | 内容 | +--------+--------+--------------------------------+ | 0 | 4 | NumSprites (uint32, little-endian)| | 4 | 4 | Offset[0] | | 8 | 4 | Offset[1] | | ... | ... | ... | | Offset[0] | 1 | Type (0x05) | | +1 | 2 | Width (uint16, little-endian) | | +3 | 2 | Height (uint16, little-endian) | | +5 | 1 | PaletteIndex (调色板索引) | | +6 | 1 | Unknown | | +7 | 1 | Unknown | | +8 | W×H | PixelData (8-bit 索引) | +--------+--------+--------------------------------+R: 5 bits(bit 11-15)G: 6 bits(bit 5-10)B: 5 bits(bit 0-4)Barge256x256.RGB)Ocean256x256.rmap8)fileSize - 1128251Offset[i+1] - Offset[i](字节)Texture.rmap8→Texture.clutTextureGenAlpha.clut、TextureExpand.clut等Texture256x256.rmap8→Texture.clutOcean.clut、Grass.clut、Sand.clut等