地图编译器:vbsp、vvis、vrad
一张 .bsp 地图不是一次生成的,而是由三个离线工具依次改写同一个文件:vbsp 把 Hammer 的笔刷与实体编译成 BSP 树、面、碰撞与道具数据,并输出门户文件;vvis 根据门户求出簇之间的潜在可见集(PVS);vrad 计算光照贴图、环境光立方与道具光照。本页按“输入 → 阶段 → 写入哪些 lump”的方式解释三者,并指出运行时引擎在哪里消费这些数据。它是 内容工具链 中地图部分的展开。
相关页面:内容工具链 · 地图加载与碰撞追踪 · 实体复制 · 实体句柄、实体列表与空间分区 · 构建与物理组织
相关源码文件#
- 共享 BSP 读写与格式:
utils/common/bsplib.cpp、public/bspfile.h、public/gamebspfile.h - vbsp:
utils/vbsp/vbsp.cpp、utils/vbsp/map.cpp、utils/vbsp/csg.cpp、utils/vbsp/brushbsp.cpp、utils/vbsp/portals.cpp、utils/vbsp/faces.cpp、utils/vbsp/writebsp.cpp、utils/vbsp/prtfile.cpp、utils/vbsp/staticprop.cpp、utils/vbsp/detailobjects.cpp、utils/vbsp/disp_vbsp.cpp - vvis:
utils/vvis_launcher/vvis_launcher.cpp、utils/vvis/vvis.cpp、utils/vvis/flow.cpp、utils/vvis/mpivis.cpp - vrad:
utils/vrad_launcher/vrad_launcher.cpp、utils/vrad/vraddll.cpp、utils/vrad/vrad.cpp、utils/vrad/lightmap.cpp、utils/vrad/vismat.cpp、utils/vrad/radial.cpp、utils/vrad/leaf_ambient_lighting.cpp、utils/vrad/vradstaticprops.cpp、utils/vrad/vraddetailprops.cpp、utils/vrad/mpivrad.cpp - 运行时消费者:
engine/cmodel.cpp、engine/mod_vis.cpp、engine/sv_main.cpp、engine/modelloader.cpp、engine/staticpropmgr.cpp
1. 总览:三个工具接力改写同一个 BSP#
证据:vbsp 只在未泄漏时写 .prt:utils/vbsp/vbsp.cpp:391-394;vvis 读取 BSP 与 .prt 后计算并写回:utils/vvis/vvis.cpp:1127-1180;vrad 加载、计算并写回:utils/vrad/vrad.cpp:3058-3067、utils/vrad/vrad.cpp:2400-2412。各阶段写入的 lump 见下文各节。
三个工具共用 utils/common/bsplib.cpp:LoadBSPFile 把各 lump 读入全局数组(例如可见性、光照、叶子到水距离、pak 文件):utils/common/bsplib.cpp:2650、utils/common/bsplib.cpp:2693-2694、utils/common/bsplib.cpp:2734、utils/common/bsplib.cpp:2749;WriteBSPFile 再把所有全局数组按引擎读取顺序写回,并追加 game lump 与 pak 文件,文件头标识 VBSP、版本 21:utils/common/bsplib.cpp:3085-3221、public/bspfile.h:23-27。所以每个工具都是“读全部 → 改一部分 → 写全部”。
| lump(节选) | 由谁生成 | 运行时谁读取 | 证据 |
|---|---|---|---|
LUMP_PLANES/NODES/LEAFS/FACES/BRUSHES/MODELS… | vbsp | 碰撞模型与渲染(见 地图加载) | public/bspfile.h:282-303、utils/common/bsplib.cpp:3118-3146 |
LUMP_ENTITIES | vbsp UnparseEntities | server.dll 实体解析 | utils/vbsp/writebsp.cpp:1358-1359、utils/common/bsplib.cpp:3130 |
LUMP_VISIBILITY | vvis | CM_DecompressVis | utils/common/bsplib.cpp:3128、engine/cmodel.cpp:3335-3392 |
LUMP_LIGHTING / LUMP_LIGHTING_HDR | vrad(LDR/HDR 各一次) | CModelLoader | utils/common/bsplib.cpp:3163-3164、engine/modelloader.cpp:1341 |
LUMP_LEAF_AMBIENT_* | vrad | CModelLoader | utils/common/bsplib.cpp:3171-3174、engine/modelloader.cpp:2642 |
game lump sprp(静态道具) | vbsp | CStaticPropMgr | public/gamebspfile.h:26-29、engine/staticpropmgr.cpp:1662-1670 |
game lump dprp/dplt/dplh(细节道具及其光照) | vbsp / vrad | 客户端细节物体系统 | game/client/detailobjectsystem.cpp:1552-1564 |
2. vbsp:笔刷到 BSP#
2.1 入口与模式#
main → RunVBSP;vbsp 把线程数强制设为 1(注释:多线程没有帮助),并加载 vphysics.dll 与表面属性:utils/vbsp/vbsp.cpp:1750、utils/vbsp/vbsp.cpp:1594-1603、utils/vbsp/vbsp.cpp:1144-1147。输入文件无扩展名时优先找 .vmm(manifest),再找 .vmf:utils/vbsp/vbsp.cpp:1621-1631。
| 模式 | 行为 | 证据 |
|---|---|---|
-onlyents | 读已有 BSP,只重新解析实体、重发静态道具并改写实体 lump;往 pak 中写 stale.txt 标记光照可能过期 | utils/vbsp/vbsp.cpp:1645-1680 |
-onlyprops | 读已有 BSP,只重发静态道具与细节道具 | utils/vbsp/vbsp.cpp:1681-1698 |
| 常规 | LoadMapFile → 可见性提示笔刷、cubemap 修正 → SetModelNumbers → ProcessModels | utils/vbsp/vbsp.cpp:1699-1728 |
LoadMapFile 对 .vmm 先加载 manifest,对 .vmf 用 chunk 文件读取器注册 world/entity 处理器:utils/vbsp/map.cpp:3273-3336。func_detail 的笔刷在加载时并入世界实体(带 CONTENTS_DETAIL),实体本身被清空:utils/vbsp/map.cpp:1978-1986。
2.2 ProcessModels 与世界模型流水线#
ProcessModels 先 BeginBSPFile、标记无动态阴影面、发出位移面初始信息与遮挡体,然后对每个带笔刷的实体:0 号实体(世界)走 ProcessWorldModel,其余(func_* 刷子实体)走 ProcessSubModel;最后生成默认 cubemap 并 EndBSPFile:utils/vbsp/vbsp.cpp:1095-1141。
证据(节点/边对应源码):
- 分块与每块 CSG/BSP:
utils/vbsp/vbsp.cpp:176-214;空块直接成为实心叶子:utils/vbsp/vbsp.cpp:198-205。块树拼接:utils/vbsp/vbsp.cpp:120-167、utils/vbsp/vbsp.cpp:297-306。 - 两遍优化循环、门户、泄漏检测、可见面标记:
utils/vbsp/vbsp.cpp:282-343。FloodEntities把所有带原点的实体放入树中,只有“有实体在内部且外部节点没被占据”才算封闭:utils/vbsp/portals.cpp:784-848;FillOutside:utils/vbsp/portals.cpp:1462-1472。 - 面生成与后处理:
utils/vbsp/vbsp.cpp:345-397。MakeFaces_r在空叶子的门户上生成面,在节点上合并并细分:utils/vbsp/faces.cpp:1434-1471;SubdivideFace按光照贴图坐标轴切分,SURF_NOLIGHT面跳过:utils/vbsp/faces.cpp:1186-1215。 BrushBSP以包围盒笔刷为根体积递归建树;BuildTree_r每次用SelectSplitSide选分割面,选不出即成为叶子:utils/vbsp/brushbsp.cpp:1393-1467、utils/vbsp/brushbsp.cpp:1334-1381。- 子模型使用全坐标范围、
FULL_DETAIL,没有根节点时报错退出(否则引擎会得到负的叶子索引):utils/vbsp/vbsp.cpp:406-433、utils/vbsp/vbsp.h:471-476。
2.3 写出 BSP#
WriteBSP 发出叶子面列表与节点树、只为世界发出区域门户、追加属于该模型的位移面,并发出水体:utils/vbsp/writebsp.cpp:971-1013。EndBSPFile 统一完成其余数据并写盘:
| 步骤 | 产物 | 证据 |
|---|---|---|
EmitBrushes、EmitPlanes、SaveVertexNormals | 笔刷、平面、顶点法线 | utils/vbsp/writebsp.cpp:1323-1327 |
UpdateAllFaceLightmapExtents | 每个面的光照贴图范围(vrad 使用) | utils/vbsp/writebsp.cpp:1329-1330 |
EmitDispLMAlphaAndNeighbors | 位移面几何、光照贴图 alpha 与邻接 | utils/vbsp/writebsp.cpp:1332-1333、utils/vbsp/disp_vbsp.cpp:532-545 |
覆盖层、EmitPhysCollision | 贴花覆盖层、物理碰撞(依赖位移信息) | utils/vbsp/writebsp.cpp:1335-1340 |
ClearDistToClosestWater | 叶子到水距离先置 0,等 vvis 计算 | utils/vbsp/writebsp.cpp:1342-1344 |
EmitStaticProps、EmitDetailObjects | 静态/细节道具 game lump | utils/vbsp/writebsp.cpp:1346-1350 |
UnparseEntities、CompactTexinfos、WriteBSPFile | 实体 lump、精简纹理信息、写盘 | utils/vbsp/writebsp.cpp:1358-1370 |
门户文件:WritePortalFile 重新生成门户、把叶子编号为可见性簇,写出 PRT1 头、簇数、门户数与各门户,再把簇号写回叶子:utils/vbsp/prtfile.cpp:21、utils/vbsp/prtfile.cpp:323-373。
2.4 静态道具与细节道具#
- 静态道具:
EmitStaticProps先收集info_lighting,再为每个prop_static/static_prop读取原点、角度、模型、碰撞、皮肤、淡出距离与若干开关标志:utils/vbsp/staticprop.cpp:1922-2010。存在合并规则时先尝试把相近道具合并成簇模型,否则直接AddStaticPropToLump;处理过的实体从实体 lump 中剥离:utils/vbsp/staticprop.cpp:2075-2095、utils/vbsp/staticprop.cpp:2215-2230。每个道具用碰撞模型包围盒(必要时凸包)求出所在叶子列表:utils/vbsp/staticprop.cpp:487-511、utils/vbsp/staticprop.cpp:539-596。结果写成sprpgame lump(字典 + 道具 + 叶子三段):utils/vbsp/staticprop.cpp:603-614。因此静态道具在运行时不是实体,而是由引擎CStaticPropMgr管理并使用特殊句柄序列号(见 实体句柄)。 - 细节道具:
EmitDetailModels收集func_detail_blocker,遍历每个面,按材质变量%detailtype查细节类型字典,以 Hammer 面 ID 作随机种子(编译结果可复现),在普通面或位移面上撒点:utils/vbsp/detailobjects.cpp:856-930;按叶子排序后写成dprpgame lump:utils/vbsp/detailobjects.cpp:815-830、utils/vbsp/detailobjects.cpp:988-999。
3. vvis:门户流动求 PVS#
3.1 入口#
vvis_launcher 加载 vvis_dll.dll,取 ILaunchableDLL 接口调用其 main:utils/vvis_launcher/vvis_launcher.cpp:48-74、utils/vvis/vvis.cpp:1235-1244。main 初始化后设置 VMPI 并进入 RunVVis:utils/vvis/vvis.cpp:1213-1230。RunVVis 依次 LoadBSPFile、解析实体与可选的可见半径、LoadPortals(缺少 .prt 或魔数不符直接报错)、CalcVis、CalcPAS、计算可见雾体积与叶子到水距离,最后写回 BSP;若指定了簇追踪参数则只输出调试轨迹不写 BSP:utils/vvis/vvis.cpp:1127-1193、utils/vvis/vvis.cpp:456-461。
3.2 计算流程#
证据:
- “基本 vis”:
BasePortalVis对每对门户做平面前后测试(启用可见半径时再按距离剔除),SimpleFlood沿portalfront洪泛得到portalflood:utils/vvis/flow.cpp:710-790、utils/vvis/flow.cpp:680-703。 - “完整 vis”:
PortalFlow以portalflood为上界递归穿越叶子,RecursiveLeafFlow用源门户、通过门户与目标门户构造分离平面逐级裁剪,能通过的门户才置入portalvis:utils/vvis/flow.cpp:627-657、utils/vvis/flow.cpp:478-500、utils/vvis/flow.cpp:593-616。-fast直接用portalflood作为结果:utils/vvis/vvis.cpp:289-302、utils/vvis/vvis.cpp:919-923。 CalcVis的编排(含 MPI 分支):utils/vvis/vvis.cpp:330-366。ClusterMerge把簇内所有门户的可见位并起来转成簇位,并把自身置为可见:utils/vvis/vvis.cpp:196-240。CompressAndCrosscheckClusterVis若 A 看得见 B 而 B 看不见 A,就清除 A→B 的位,使 PVS 对称,然后压缩并记录偏移:utils/vvis/vvis.cpp:242-281。CalcPAS对每个簇把其 PVS 中所有簇的 PVS 行再 OR 一遍(“可听集”),压缩后写入DVIS_PAS偏移:utils/vvis/vvis.cpp:568-629。
3.3 存储格式#
可见性 lump 以 dvis_t 开头:簇数 + 每簇 [DVIS_PVS, DVIS_PAS] 两个字节偏移,后接压缩位串:public/bspfile.h:1007-1016。压缩只对零字节做游程编码(非零字节原样,零字节后跟重复数,最长 255):utils/common/bsplib.cpp:1620-1648。
3.4 运行时如何使用 PVS#
- 解压:
CM_DecompressVis在簇号越界或地图没有可见性数据时返回“全部可见”,解压溢出时只告警:engine/cmodel.cpp:3335-3392;CM_Vis/CM_ClusterPVS为调用方提供解压缓冲:engine/cmodel.cpp:3401-3440。 - 网络裁剪:服务端为每个客户端调用
ClientSetupVisibility,游戏代码ResetPVS后把视点(及分屏玩家视点)加入;引擎按“点所在叶子 → 簇”把簇的 PVS 并入“胖 PVS”,并记录涉及的区域:game/server/gameinterface.cpp:3418-3451、engine/sv_main.cpp:1708-1745。之后CheckTransmit用它决定发送哪些实体(见 实体复制)。 - 渲染:
Map_VisMark取主视点簇的 PVS,再把额外视点簇的 PVS 按 32 位并入,用于标记可见叶子:engine/mod_vis.cpp:310-330。
4. vrad:光照#
4.1 入口与 LDR/HDR#
vrad_launcher 可通过同目录 vrad.redirect 换用其他 DLL,否则加载 vrad_dll.dll;命令行含 -both 时循环两次,把该参数分别替换为 -ldr、-hdr:utils/vrad_launcher/vrad_launcher.cpp:58-79、utils/vrad_launcher/vrad_launcher.cpp:99-143。DLL 的 main 转到 VRAD_Main → RunVRAD:utils/vrad/vraddll.cpp:110-112、utils/vrad/vrad.cpp:3077-3104、utils/vrad/vrad.cpp:3037-3074。-hdr/-ldr 调用 SetHDRMode,切换光照数据、叶子环境光与世界光源写入的目标数组:utils/vrad/vrad.cpp:2723-2730、utils/common/bsplib.cpp:4352-4379。
4.2 加载与准备#
VRAD_LoadBSP:读取必需的全局 lights.rad(找不到则去可执行文件目录)与可选的关卡 .rad,加载 BSP,设置关卡标志,HDR 模式下复制一份 HDR 面数组,初始化静态道具与位移管理器:utils/vrad/vrad.cpp:2238-2313。没有可见性数据时只算直接光:反弹次数置 0、环境光置 0.1,并从叶子推算簇数:utils/vrad/vrad.cpp:2315-2321。随后把笔刷、位移、静态道具多边形加入光线追踪环境并建立加速结构:utils/vrad/vrad.cpp:2346-2361。
RadWorld_Start:按 luxeldensity 放宽光照贴图密度,MakePatches 把每个面变成补丁,PairEdges 计算并保存平滑顶点法线,SubdividePatches 细分补丁,加入位移(及可选静态道具)补丁,CreateDirectLights 从光源实体与发光补丁创建直接光源,处理天空相机:utils/vrad/vrad.cpp:1783-1864。
4.3 直接光与辐射度反弹:RadWorld_Go#
证据:
- 编排:
utils/vrad/vrad.cpp:2050-2177。BuildFacelights先把面的lightofs置 -1,跳过不被任何光源照到的面与TEX_SPECIAL面:utils/vrad/lightmap.cpp:3401-3440。 MakeAllScales调BuildVisMatrix:utils/vrad/vrad.cpp:1959-1966;后者按簇分配工作,每个簇先用 vvis 的 PVS 解压出可见簇,再为簇内每个补丁建立到其他补丁的能量传递(transfer):utils/vrad/vismat.cpp:505-515、utils/vrad/vismat.cpp:423-469。BounceLight把直接光移到“待发射”数组,循环收集反弹,直到达到numbounce或某次新增能量三通道都小于 1:utils/vrad/vrad.cpp:1682-1756。numbounce默认 100,可用-bounce修改:utils/vrad/vrad.cpp:52、utils/vrad/vrad.cpp:2525-2532。FinalLightFace注释即“把间接光加到直接光上并存成最终格式”,按凹凸采样写入当前模式的光照数组:utils/vrad/radial.cpp:634-660、utils/vrad/radial.cpp:739。
4.4 其他光照:VRAD_ComputeOtherLighting#
| 计算 | 条件 | 产物 | 证据 |
|---|---|---|---|
| 细节道具光照 | 未指定关闭 | game lump dplt / dplh | utils/vrad/vrad.cpp:2384-2387、utils/vrad/vraddetailprops.cpp:1284-1285 |
| 每叶子环境光立方 | 总是 | LUMP_LEAF_AMBIENT_INDEX(_HDR) + LUMP_LEAF_AMBIENT_LIGHTING(_HDR) | utils/vrad/vrad.cpp:2389、utils/vrad/leaf_ambient_lighting.cpp:620-680 |
| 静态道具逐顶点光照 | 需 -StaticPropLighting 且未用 -fast(默认关闭) | 每个道具一个 sp_N.vhv / sp_hdr_N.vhv,放进 BSP 的 pak lump | utils/vrad/vrad.cpp:2391-2395、utils/vrad/vrad.cpp:121、utils/vrad/vrad.cpp:2458-2463、utils/vrad/vradstaticprops.cpp:2067-2095、utils/vrad/vradstaticprops.cpp:2163 |
环境光立方的采样方法:从采样点向 NUMVERTEXNORMALS 个方向发射射线取命中处的光,再按与 6 个轴向的点积加权累加成“六面盒”:utils/vrad/leaf_ambient_lighting.cpp:139-170。写出时样本位置用叶子包围盒内的 8 位定点比例编码:utils/vrad/leaf_ambient_lighting.cpp:672-680。静态道具光照计算期间忽略对静态道具自身的遮挡追踪,完成后序列化:utils/vrad/vradstaticprops.cpp:2253-2302。最后 VRAD_Finish 写回 BSP:utils/vrad/vrad.cpp:2400-2436。
5. 多线程与 VMPI 分布式#
- 线程:vvis 与 vrad 的主要循环用
RunThreadsOnIndividual/RunThreadsOn分发到工作线程(如PortalFlow、BuildFacelights、FinalLightFace、BuildVisLeafs):utils/vvis/vvis.cpp:305-312、utils/vrad/vrad.cpp:2081-2087、utils/vrad/vrad.cpp:2166-2168、utils/vrad/vismat.cpp:511-514;vbsp 则固定单线程(见 2.1)。 - VMPI:命令行带
-mpi(或工作者参数)时才初始化 VMPI,可选本地模式:utils/vvis/mpivis.cpp:130-155、utils/vrad/mpivrad.cpp:71-96。可分布的阶段包括 vvis 的基本 vis 与门户流动、vrad 的直接光、可见性矩阵、叶子环境光与静态道具光照:utils/vvis/mpivis.cpp:208、utils/vvis/mpivis.cpp:521、utils/vrad/mpivrad.cpp:232、utils/vrad/mpivrad.cpp:374、utils/vrad/leaf_ambient_lighting.cpp:645-654、utils/vrad/vradstaticprops.cpp:2276-2289。VMPI 库本身只以预编译库形式存在(见 内容工具链)。
约束与边界#
- 顺序依赖:vvis 需要 vbsp 产生的
.prt;地图泄漏时 vbsp 不写.prt,vvis 的LoadPortals会报错退出(由utils/vbsp/vbsp.cpp:391-394与utils/vvis/vvis.cpp:456-461推出)。vrad 在没有可见性数据时仍能运行,但只有直接光。 - 整文件重写:每个工具都重写全部 lump,因此重新运行 vbsp(非
-onlyents/-onlyprops)会丢掉之前的可见性与光照,需要重跑 vvis/vrad;-onlyents用stale.txt标记光照可能过期。 - 外部依赖:vbsp 与 vrad 运行时加载
vphysics.dll(碰撞生成、静态道具碰撞模型),而仓库缺少 vphysics 源码(见 构建与物理组织):utils/vbsp/vbsp.cpp:1144-1147、utils/vrad/vrad.cpp:2183-2186。 - HDR:一次运行只写 LDR 或 HDR 一套光照;同时需要两套时依赖 launcher 的
-both。 - 未执行:本页未运行任何编译器,没有样例地图;所有流程均为静态阅读。
- 未展开:CSG 细节(
ChopBrushes)、SelectSplitSide的启发式、T 型接缝修复、区域门户/遮挡体、位移面邻接缝合、覆盖层、cubemap 采样、光照贴图超采样与凹凸光照、辐射度 transfer 的数学形式、静态道具合并规则、增量光照(g_pIncremental)。