CD Code Digest Hub /CS:GO

地图编译器:vbsp、vvis、vrad

31 分钟阅读 7 个章节 4 张图

一张 .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_ENTITIESvbsp UnparseEntitiesserver.dll 实体解析utils/vbsp/writebsp.cpp:1358-1359、utils/common/bsplib.cpp:3130
LUMP_VISIBILITYvvisCM_DecompressVisutils/common/bsplib.cpp:3128、engine/cmodel.cpp:3335-3392
LUMP_LIGHTING / LUMP_LIGHTING_HDRvrad(LDR/HDR 各一次)CModelLoaderutils/common/bsplib.cpp:3163-3164、engine/modelloader.cpp:1341
LUMP_LEAF_AMBIENT_*vradCModelLoaderutils/common/bsplib.cpp:3171-3174、engine/modelloader.cpp:2642
game lump sprp(静态道具)vbspCStaticPropMgrpublic/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 → ProcessModelsutils/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 lumputils/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。结果写成 sprp game lump(字典 + 道具 + 叶子三段):utils/vbsp/staticprop.cpp:603-614。因此静态道具在运行时不是实体,而是由引擎 CStaticPropMgr 管理并使用特殊句柄序列号(见 实体句柄)。
  • 细节道具:EmitDetailModels 收集 func_detail_blocker,遍历每个面,按材质变量 %detailtype 查细节类型字典,以 Hammer 面 ID 作随机种子(编译结果可复现),在普通面或位移面上撒点:utils/vbsp/detailobjects.cpp:856-930;按叶子排序后写成 dprp game 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 / dplhutils/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 lumputils/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)。