perf(renderer): 实现 Raw 8-bit GPU 索引纹理 (R8) + Shader 硬件调色板查表架构 #34

Closed
opened 2026-09-16 01:54:23 +00:00 by troytt · 1 comment
Owner

背景与目标

当前预打包实体图集(人物与怪物)采用 8-bit Indexed PNG(Color Type 3),虽然相比运行时 MPQ HTTP Range 解码已将加载时间缩短至 <200ms,但仍存在以下可进一步优化的瓶颈:

  1. 图像解码与 RGBA 展开开销:浏览器仍需经过 Blob -> ImageDecoder -> RGBA8 像素展开 过程,上传至 GPU 后占用 4 字节/像素(1024×1024 图集占 4MB 显存)。
  2. 跨幕冗余存储:暗黑 2 同一怪物在 Act 1..5 的像素索引完全一致,仅 pal.pl2 调色板不同。使用 PNG 时需要在 act1..5/entities/ 重复存储 5 套图集。
  3. 无法支持原版动态调色板变换(Color Shift):暗黑 2 的头目/精英怪染色(RandTransforms.dat / palshift.dat)、冰冻变蓝(Cold)、中毒变绿(Poison)均基于 8-bit 调色板索引重映射,固定 RGBA 纹理无法动态换色。

核心架构设计

1. 全局单套 Raw 8-bit 索引图集 (.r8 + .json)

  • 构建期将 char-so 及全部 66 种怪物图集打包为 全局唯一 的 samples/d2-packs/entities/<slug>.r8(纯 Uint8Array 1 byte/pixel 原始索引流)与 <slug>.json。
  • 像素值 0 代表透明(mask === 0),非透明但原索引为 0(纯黑)的像素自动重映射至最接近的纯黑非零索引(如索引 255 或由调色板决定的黑点),确保 index == 0 严格对应透明。
  • .r8 文件体积仅 ~30KB–120KB(传输层天然支持 Gzip/Brotli 压缩),彻底消除跨幕重复存储(总实体包体减少 80%)。

2. WebGL2 R8 单通道纹理与硬件调色板纹理 (SpriteRenderer)

  • 图集纹理 (gl.R8):在 SpriteRenderer 中支持 addIndexedAtlas(indices: Uint8Array, width: number, height: number),使用 gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, width, height, 0, gl.RED, gl.UNSIGNED_BYTE, indices) 直接上传 1 byte/pixel 纹理(NEAREST 采样,显存占用与 PCIe 带宽降低 75%,CPU 解码耗时 0ms)。
  • 调色板纹理 (uPaletteTex):将当前幕的 256 色调色板(及后续扩展的多行染色表,如冰冻蓝、中毒绿、精英染色行)上传为 256 × N 的 RGBA8 纹理。

3. Fragment Shader 硬件级查表与染色支持

  • 在 Fragment Shader 中通过 uUsePalette(或纹理模式)区分普通 RGBA 瓦片图集与 R8 索引实体图集:
    • 当采样 R8 索引图集时,读取红色通道 float idx = texture(uAtlas, vUv).r * 255.0;;
    • 若 idx < 0.5(即索引 0),直接 discard;
    • 否则通过 texelFetch(uPalette, ivec2(int(idx + 0.5), vPaletteRow), 0) 从硬件调色板纹理取出精确 RGBA 颜色输出。
  • 支持在 renderer.draw(frame, x, y, { atlas, paletteRow }) 中传入 paletteRow(如 0 = 默认原色,1 = 冰冻蓝 tint,2 = 中毒绿 tint,3 = 精英金怪/蓝怪 tint),实现零显存开销的原版状态染色!
## 背景与目标 当前预打包实体图集(人物与怪物)采用 8-bit Indexed PNG(Color Type 3),虽然相比运行时 MPQ HTTP Range 解码已将加载时间缩短至 <200ms,但仍存在以下可进一步优化的瓶颈: 1. **图像解码与 RGBA 展开开销**:浏览器仍需经过 `Blob -> ImageDecoder -> RGBA8 像素展开` 过程,上传至 GPU 后占用 **4 字节/像素**(1024×1024 图集占 4MB 显存)。 2. **跨幕冗余存储**:暗黑 2 同一怪物在 Act 1..5 的像素索引完全一致,仅 `pal.pl2` 调色板不同。使用 PNG 时需要在 `act1..5/entities/` 重复存储 5 套图集。 3. **无法支持原版动态调色板变换(Color Shift)**:暗黑 2 的头目/精英怪染色(`RandTransforms.dat` / `palshift.dat`)、冰冻变蓝(Cold)、中毒变绿(Poison)均基于 8-bit 调色板索引重映射,固定 RGBA 纹理无法动态换色。 --- ## 核心架构设计 ### 1. 全局单套 Raw 8-bit 索引图集 (`.r8` + `.json`) - 构建期将 `char-so` 及全部 66 种怪物图集打包为 **全局唯一** 的 `samples/d2-packs/entities/<slug>.r8`(纯 `Uint8Array` 1 byte/pixel 原始索引流)与 `<slug>.json`。 - 像素值 `0` 代表透明(`mask === 0`),非透明但原索引为 `0`(纯黑)的像素自动重映射至最接近的纯黑非零索引(如索引 `255` 或由调色板决定的黑点),确保 `index == 0` 严格对应透明。 - `.r8` 文件体积仅 ~30KB–120KB(传输层天然支持 Gzip/Brotli 压缩),彻底消除跨幕重复存储(**总实体包体减少 80%**)。 ### 2. WebGL2 `R8` 单通道纹理与硬件调色板纹理 (`SpriteRenderer`) - **图集纹理 (`gl.R8`)**:在 `SpriteRenderer` 中支持 `addIndexedAtlas(indices: Uint8Array, width: number, height: number)`,使用 `gl.texImage2D(gl.TEXTURE_2D, 0, gl.R8, width, height, 0, gl.RED, gl.UNSIGNED_BYTE, indices)` 直接上传 1 byte/pixel 纹理(`NEAREST` 采样,**显存占用与 PCIe 带宽降低 75%,CPU 解码耗时 0ms**)。 - **调色板纹理 (`uPaletteTex`)**:将当前幕的 256 色调色板(及后续扩展的多行染色表,如冰冻蓝、中毒绿、精英染色行)上传为 `256 × N` 的 `RGBA8` 纹理。 ### 3. Fragment Shader 硬件级查表与染色支持 - 在 Fragment Shader 中通过 `uUsePalette`(或纹理模式)区分普通 `RGBA` 瓦片图集与 `R8` 索引实体图集: - 当采样 `R8` 索引图集时,读取红色通道 `float idx = texture(uAtlas, vUv).r * 255.0;`; - 若 `idx < 0.5`(即索引 0),直接 `discard`; - 否则通过 `texelFetch(uPalette, ivec2(int(idx + 0.5), vPaletteRow), 0)` 从硬件调色板纹理取出精确 RGBA 颜色输出。 - 支持在 `renderer.draw(frame, x, y, { atlas, paletteRow })` 中传入 `paletteRow`(如 `0` = 默认原色,`1` = 冰冻蓝 tint,`2` = 中毒绿 tint,`3` = 精英金怪/蓝怪 tint),实现零显存开销的原版状态染色!
Author
Owner

✅ 已完成 Raw 8-bit GPU 索引纹理 (gl.R8) + Shader 硬件调色板查表架构实现 (Commit d4fb176)

1. WebGL2 渲染器升级 (src/render/renderer.ts)

  • 单通道 8-bit 索引纹理 (gl.R8):新增 addIndexedAtlas(indices: Uint8Array, width: number, height: number): AtlasHandle,直接将 Uint8Array 字节流上传至 GPU VRAM (gl.R8 / gl.RED / gl.UNSIGNED_BYTE):
    • 0ms CPU 解码:完全跳过 createImageBitmap 与 PNG zlib 解压;
    • 75% 显存缩减:每像素仅占 1 Byte(相比 RGBA 4 Bytes/pixel 降低 75%);
    • 全自动 sampler2D 兼容:与现有 RGBA 图集共享同一纹理单元数组 (u_textures[0..14]),零额外 Draw Call 开销。
  • 硬件调色板纹理 (u_palette, 256×5 RGBA):新增 setPalette(rgb: Uint8Array),在专用纹理单元 (this.paletteUnit) 构建 256×5 硬件调色板查找表:
    • Row 0:当前幕(Act 1..5)标准调色板;
    • Row 1:冰冻/寒冷减速蓝(Cold Chilled Blue);
    • Row 2:中毒绿(Poison Green);
    • Row 3:暗金/精英金(Unique Boss Gold);
    • Row 4:头目/冠军蓝(Champion Elite Blue)。
  • 顶点属性与 Fragment Shader 查表:
    • 顶点跨度扩展至 FLOATS_PER_VERTEX = 10(新增 a_palette 属性);
    • 当 v_palette >= 0 时,Fragment Shader 执行 int idx = int(texel.r * 255.0 + 0.5) 并通过 texelFetch(u_palette, ivec2(idx, row), 0) 完成硬件级实时着色。

2. 全局跨幕无关 .r8 实体图集烘焙 (scripts/pack-entity-assets.ts & src/render/atlas.ts)

  • 利用暗黑 2 全五幕调色板中 Index 0 为透明色 (mask=0)、Index 172 恒为纯黑 (rgb(4,4,4)) 的特性,实现 buildIndexedAtlas / packSheetToR8:
    • 将不透明黑像素 (mask=1 && index=0) 统一重映射至 172;
    • 烘焙生成 samples/d2-packs/entities/char-so.{r8,json} 及全部 66 种跨幕怪物 的全局 monster-<token>-<weapon>.{r8,json};
    • 消除 80% 跨幕冗余:Act 1..5 共享同一份 .r8 索引图集,切换幕数时仅需调用 renderer.setPalette(runtime.palette.rgb) 即可切换幕色调。

3. 场景加载与自动化测试验证 (src/scene/act-scene.ts & tests/packed-entity-art.test.ts)

  • runScene 加载幕调色板时自动调用 renderer.setPalette(runtime.palette.rgb);
  • loadCharacterArt 与 loadMonsterArtMap 优先拉取全局 entities/*.r8 + .json 并通过 addIndexedAtlas 直传 GPU,同时保留按幕 PNG 与 MPQ 实时提取作为后备;
  • 新增单元测试验证全部 67 份全局 .r8 图集与尺寸完整性,537 项测试全部通过。
## ✅ 已完成 Raw 8-bit GPU 索引纹理 (`gl.R8`) + Shader 硬件调色板查表架构实现 (Commit `d4fb176`) ### 1. WebGL2 渲染器升级 (`src/render/renderer.ts`) - **单通道 8-bit 索引纹理 (`gl.R8`)**:新增 `addIndexedAtlas(indices: Uint8Array, width: number, height: number): AtlasHandle`,直接将 `Uint8Array` 字节流上传至 GPU VRAM (`gl.R8` / `gl.RED` / `gl.UNSIGNED_BYTE`): - **0ms CPU 解码**:完全跳过 `createImageBitmap` 与 PNG zlib 解压; - **75% 显存缩减**:每像素仅占 1 Byte(相比 RGBA 4 Bytes/pixel 降低 75%); - **全自动 `sampler2D` 兼容**:与现有 RGBA 图集共享同一纹理单元数组 (`u_textures[0..14]`),零额外 Draw Call 开销。 - **硬件调色板纹理 (`u_palette`, 256×5 RGBA)**:新增 `setPalette(rgb: Uint8Array)`,在专用纹理单元 (`this.paletteUnit`) 构建 256×5 硬件调色板查找表: - `Row 0`:当前幕(Act 1..5)标准调色板; - `Row 1`:冰冻/寒冷减速蓝(Cold Chilled Blue); - `Row 2`:中毒绿(Poison Green); - `Row 3`:暗金/精英金(Unique Boss Gold); - `Row 4`:头目/冠军蓝(Champion Elite Blue)。 - **顶点属性与 Fragment Shader 查表**: - 顶点跨度扩展至 `FLOATS_PER_VERTEX = 10`(新增 `a_palette` 属性); - 当 `v_palette >= 0` 时,Fragment Shader 执行 `int idx = int(texel.r * 255.0 + 0.5)` 并通过 `texelFetch(u_palette, ivec2(idx, row), 0)` 完成硬件级实时着色。 ### 2. 全局跨幕无关 `.r8` 实体图集烘焙 (`scripts/pack-entity-assets.ts` & `src/render/atlas.ts`) - 利用暗黑 2 全五幕调色板中 **Index 0 为透明色 (`mask=0`)**、**Index 172 恒为纯黑 (`rgb(4,4,4)`)** 的特性,实现 `buildIndexedAtlas` / `packSheetToR8`: - 将不透明黑像素 (`mask=1 && index=0`) 统一重映射至 `172`; - 烘焙生成 `samples/d2-packs/entities/char-so.{r8,json}` 及全部 **66 种跨幕怪物** 的全局 `monster-<token>-<weapon>.{r8,json}`; - **消除 80% 跨幕冗余**:Act 1..5 共享同一份 `.r8` 索引图集,切换幕数时仅需调用 `renderer.setPalette(runtime.palette.rgb)` 即可切换幕色调。 ### 3. 场景加载与自动化测试验证 (`src/scene/act-scene.ts` & `tests/packed-entity-art.test.ts`) - `runScene` 加载幕调色板时自动调用 `renderer.setPalette(runtime.palette.rgb)`; - `loadCharacterArt` 与 `loadMonsterArtMap` 优先拉取全局 `entities/*.r8` + `.json` 并通过 `addIndexedAtlas` 直传 GPU,同时保留按幕 PNG 与 MPQ 实时提取作为后备; - 新增单元测试验证全部 67 份全局 `.r8` 图集与尺寸完整性,**537 项测试全部通过**。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#34
No description provided.