CD Code Digest Hub /CS:GO

引擎世界渲染:BSP 表面、光照贴图与可见性

34 分钟阅读 12 个章节 1 张图

本页解释 engine 中“画世界”的部分:关卡加载时如何为 BSP 表面分配光照贴图并建立静态顶点缓冲,每个视图如何用 PVS、区域门户(area portal)和视锥把 BSP 树裁成一份世界列表,以及位移面、静态道具、贴花/覆盖层、投影阴影和天空盒如何挂进这份列表。客户端(客户端视图渲染管线)只决定“何时、以什么标志画世界”,真正的 BSP 遍历与表面提交都在这里。

相关页面:渲染管线总览 · 客户端视图渲染管线 · 材质系统与 Shader API · 模型、动画与粒子 · 地图加载与碰撞追踪 · 内容工具链

相关源码文件#

  • 客户端接口与渲染器:engine/view.cpp、engine/gl_rmain.cpp、engine/gl_rmisc.cpp
  • BSP 表面与世界列表:engine/gl_rsurf.cpp、engine/gl_model_private.h
  • 光照贴图:engine/gl_lightmap.cpp、engine/gl_matsysiface.cpp、engine/matsys_interface.cpp
  • 可见性:engine/mod_vis.cpp、engine/r_areaportal.cpp、game/server/playerlocaldata.cpp
  • 位移面:engine/disp_interface.cpp、engine/disp_mapload.cpp、engine/disp.cpp
  • 静态道具:engine/staticpropmgr.cpp
  • 贴花、覆盖层、阴影、天空:engine/r_decal.cpp、engine/Overlay.cpp、engine/shadowmgr.cpp、engine/gl_warp.cpp
  • 编译条件:engine/engine_inc.vpc

1. 客户端如何调用引擎画世界:IVRenderView#

引擎以 CVRenderView 实现 IVRenderView 并通过 EXPOSE_SINGLE_INTERFACE_GLOBALVAR 暴露(engine/view.cpp:272、engine/view.cpp:700);client.dll 在初始化时取得 render、shadowmgr、staticpropmgr 等全局接口(game/client/cdll_client_int.cpp:1470、game/client/cdll_client_int.cpp:1486-1488)。CVRenderView 多数方法只是转发给 g_EngineRenderer(CRender 单例,engine/gl_rmain.cpp:238、engine/gl_rmain.cpp:358)或 R_* 函数:

客户端调用引擎实现作用证据
render->SceneBegin/SceneEndCRender::DrawSceneBegin/End检查光照配置变化;场景结束同步(PS3)与叶子可视化调试engine/view.cpp:391-399、engine/gl_rmain.cpp:1180-1189
render->ViewSetupVisExMap_VisSetup用一个或多个视点标记 PVSengine/view.cpp:509-512、engine/gl_rmain.cpp:487-490
render->CreateWorldList/BuildWorldListsAllocWorldRenderList / R_BuildWorldLists分配并填充一个视图的世界列表engine/view.cpp:406-431、engine/gl_rmain.cpp:1191-1225
render->BuildWorldLists_EpilogueR_BuildWorldLists_Epilogue加贴花表面、更新动态光照贴图engine/view.cpp:422-425、engine/gl_rmain.cpp:1235-1253
render->DrawWorldListsR_DrawWorldLists按 DRAWWORLDLISTS_* 标志绘制列表engine/view.cpp:433-436、engine/gl_rmain.cpp:1256-1260
render->GetVisibleFogVolumeR_GetVisibleFogVolume查询视点所在/可见的水雾体积engine/view.cpp:401-404、engine/gl_rsurf.cpp:5047
render->SetAreaState写 CClientState::m_chAreaBits/m_chAreaPortalBits接收服务端下发的区域可见性engine/view.cpp:582-590
render->DrawBrushModelExR_DrawBrushModel画刷实体(门、func_*)engine/view.cpp:296-300、engine/gl_rsurf.cpp:5638-5660
render->DrawTranslucentSurfacesShader_DrawTranslucentSurfaces按叶子画半透明世界表面engine/view.cpp:484-487、engine/gl_rsurf.cpp:3576

除 gl_rsurf.cpp、位移面、贴花、matsys_interface.cpp、modelloader.cpp 外,gl_rmain.cpp、gl_lightmap.cpp、gl_matsysiface.cpp、r_areaportal.cpp、Overlay.cpp、shadowmgr.cpp 等都带 [!$DEDICATED],专用服务器不编译:engine/engine_inc.vpc:165-192。

2. 关卡初始化:几何、光照贴图、静态道具#

客户端在注册完资源后调用 R_LevelInit(engine/client.cpp:2102),其顺序为:重置 lightstyle、初始化贴花、加载 2D 天空盒、初始化 studio;仅客户端(!sv.IsActive())时重建空间分区并初始化静态道具与阴影管理器;无世界光源的地图强制 mat_fullbright 1;然后 CacheUsedMaterials → R_LoadWorldGeometry → 表面/区域门户初始化 → 生成覆盖层碎片:engine/gl_rmisc.cpp:260-322。监听服务器的静态道具由服务端世界初始化调用 LevelInit:engine/world.cpp:76。

R_LoadWorldGeometry:engine/gl_rmisc.cpp:185-252

  1. 光照贴图分配:MaterialSystem_RegisterLightmapSurfaces 在 BeginLightmapAllocation/EndLightmapAllocation 之间,把所有表面按“有光照贴图优先 → 材质枚举 ID → 无 lightstyle 优先 → 光照贴图面积从大到小”排序后逐个分配:engine/gl_matsysiface.cpp:306-348、engine/gl_matsysiface.cpp:265-304。有光照贴图的表面调用 materials->AllocateLightmap 得到材质排序 ID 与页内偏移(凹凸光照的宽度为 (NUM_BUMP_VECTS+1) 倍);无光照贴图的表面用 AllocateWhiteLightmap:engine/gl_matsysiface.cpp:236-263。
  2. 排序信息与静态网格:MaterialSystem_CreateSortinfo 取回材质系统的排序表、重排并重映射每个表面的排序 ID,然后 WorldStaticMeshCreate:engine/gl_matsysiface.cpp:501-550。后者为每个排序 ID 建立静态顶点缓冲,位移面表面被跳过(标记 0xFFFF),并告诉阴影管理器材质桶数量:engine/matsys_interface.cpp:1749-1823。
  3. 位移面:首次渲染该地图时 Map_LoadDisplacements → DispInfo_LoadDisplacements:engine/gl_rmisc.cpp:210-226、engine/modelloader.cpp:6048-6062;位移面数量超过 MAX_MAP_DISPINFO 时报错,逐个创建静态缓冲:engine/disp_mapload.cpp:567、engine/disp_mapload.cpp:626、engine/disp_mapload.cpp:723。
  4. 标记水面、GL_RebuildLightmaps:engine/gl_rmisc.cpp:234-237。

覆盖层(info_overlay)在地图加载时从 LUMP_OVERLAYS、LUMP_WATEROVERLAYS 及淡出/系统级别并行 lump 读入,任一 lump 大小不是结构体整数倍即返回失败:engine/modelloader.cpp:5250-5252、engine/Overlay.cpp:1800-1830。

3. 可见性:PVS → 区域门户 → 视锥#

证据:服务端 game/server/playerlocaldata.cpp:254-316、game/server/playerlocaldata.cpp:25;客户端仅主本地玩家(分屏槽 0)调用 SetAreaState:game/client/c_baseplayer.cpp:1379-1387;SetupVis:game/client/viewrender.cpp:2583-2597;R_BuildWorldLists 在非顶视图时调用 R_SetupAreaBits 后创建作业:engine/gl_rsurf.cpp:4805-4831;作业遍历:engine/gl_rsurf.cpp:4093-4110。

3.1 PVS(mod_vis.cpp)#

  • Map_VisSetup 为每个视点求叶子与簇;视点叶子带天空标志时记录天空可见,带 LEAF_FLAGS_RADIAL 时返回“使用径向可见性”标志:engine/mod_vis.cpp:378-410。
  • Map_VisMark:簇集合未变化则直接返回;否则递增 r_visframecount。视点在世界外(且未启用门户 PVS 优化或开了 noclip)、r_novis 或 forcenovis 时把所有叶子和节点标为可见;否则用 CM_Vis(..., DVIS_PVS) 取第一个簇的 PVS 并按位或合并其他簇,再查 viscache 命中则复用,未命中则构建新缓存项:engine/mod_vis.cpp:256-368。

3.2 区域门户(r_areaportal.cpp)#

  • 服务端按玩家视点(含分屏/画中画玩家)计算区域可见位并通过 m_chAreaBits、m_chAreaPortalBits 下发:game/server/playerlocaldata.cpp:254-316。
  • R_SetupAreaBits 从视点叶子所在区域出发,初始裁剪矩形为整个屏幕。视点在固体中、正在观看 HLTV、区域位尚未有效或 r_portalsopenall 时,设置 g_bViewerInSolidSpace 并“全部绘制”:engine/r_areaportal.cpp:481-543。
  • R_FlowThroughArea 递归穿过门户:跳过栈上区域、关闭的门户(m_chAreaPortalBits)、背向视点的门户以及客户端不要求可见的区域(m_chAreaBits);把门户投到屏幕得到矩形,与当前裁剪矩形求交后继续流入相邻区域,并合并该区域的屏幕矩形:engine/r_areaportal.cpp:230-330。
  • 非固体空间时为每个可见区域建立区域视锥:engine/r_areaportal.cpp:583-586。

3.3 BSP 遍历(CBuildWorldListsJob)#

  • R_RecursiveWorldNode:固体节点或 visframe < m_visFrameCount(未在 PVS 中)直接返回;用区域视锥或主视锥剔除节点(R_CullNode 先检查区域位);到达叶子调用 R_DrawLeaf;否则先递归近侧子树,再处理挂在节点上的、已被可见叶子标记过的表面(天空 → 标记天空可见,半透明 → 半透明列表,其余 → 不透明排序列表),最后迭代远侧:engine/gl_rsurf.cpp:4147-4257、engine/gl_rsurf.cpp:4403-4415。
  • 注释说明 visframe 用 < 而非 != 比较,是为了规避多视图下 PVS 缓存覆盖造成的竞态闪烁,代价是偶有误判为可见:engine/gl_rsurf.cpp:4162-4165。
  • R_DrawLeaf:记录可见叶子;加入位移面;对“节点表面”只做标记,留给 R_RecursiveWorldNode 在节点处输出;对叶子独有表面(func_detail)做背面剔除,再按标志分流到 dlight/lightstyle 列表、半透明列表或按 sort group + 材质排序 ID 加入排序列表,有贴花的表面排入贴花队列:engine/gl_rsurf.cpp:4434-4544。
  • 作业结束后统计半透明表面,并按视点是否在水雾中写入 m_ViewFogVolume,返回叶子列表(客户端按它从后往前画半透明物):engine/gl_rsurf.cpp:4112-4140。阴影深度视图改用不剔除的 R_BuildWorldListNoCull:engine/gl_rsurf.cpp:4097-4100。

关键表面标志(SURFDRAW_*):NOLIGHT 无光照贴图、NODE 挂在节点上、SKY 通往天空、TRANS 需从后往前排序、PLANEBACK 背向节点平面、NOCULL 不做背面剔除、HASLIGHTSYTLES/HASDLIGHT 需要光照贴图更新、HAS_DISP 有位移面、WATERSURFACE 水面,另有 2 位 sort group:engine/gl_model_private.h:438-469。

4. 绘制世界列表:R_DrawWorldLists#

R_DrawWorldLists 在文本模式或丢失显存时直接返回;DRAW_WORLD_GEOMETRY 走 Shader_WorldEnd,简化世界模型走 DrawSimpleWorldModel;DRAW_DECALS_AND_OVERLAYS 走 Shader_DrawWorldDecalsAndOverlays:engine/gl_rsurf.cpp:5115-5146。客户端把同一份列表分两次调用这两个分支(先几何、后贴花),见 客户端视图渲染管线 第 4 节。

Shader_WorldEnd(几何):engine/gl_rsurf.cpp:3330-3459

  1. 阴影深度 / SSAO 列表走深度填充专用路径(X360、r_csm_fast_path、普通三选一)后返回:engine/gl_rsurf.cpp:3334-3350。
  2. r_skybox_draw_last 为 0 时先画 2D 天空盒(仅当本列表看到天空或强制全天空):engine/gl_rsurf.cpp:3352-3373。
  3. PC 上 r_fastzreject 时先做 Z 填充:engine/gl_rsurf.cpp:3375-3384。
  4. 从高到低遍历 sort group(注释:先画雾体积,因为只有在雾中才画它,且更近、利于早期 Z 剔除);水面组可加 Z 偏移并启用单遍投影光;每组先画位移面链、再画普通表面链:engine/gl_rsurf.cpp:3386-3435。
  5. r_skybox_draw_last 为 1 时最后画天空盒:engine/gl_rsurf.cpp:3437-3458。

Shader_DrawWorldDecalsAndOverlays(每个 sort group):位移面贴花与覆盖层 → 投影纹理贴花 → 把表面上的阴影加入渲染列表 → 投影光 → 覆盖层 → 投影光覆盖层 → 世界贴花 → 投影光贴花 → 回收贴花 → RTT 阴影:engine/gl_rsurf.cpp:3465-3543。

由此可知(推断):世界不透明几何与贴花/覆盖层/投影阴影是两次 DrawWorldLists 调用,中间夹着客户端的不透明实体绘制。

5. 光照贴图的运行时更新#

  • 每帧开始:CRender::FrameBegin 递增 r_framecount,关卡内执行 R_AnimateLight(lightstyle 动画)与 R_PushDlights,重置光照贴图变换列表(第 0 项为世界、单位矩阵):engine/gl_rmain.cpp:377-406。
  • 收集:R_CheckForLightmapUpdates 对本帧未计算过的表面检查:lightstyle 帧号是否比上次计算新、是否本帧或之前受 dlight 影响;r_dynamic 0 或 r_keepstyledlightmapsonly 时只考虑 lightstyle;白色光照贴图页直接跳过;需要更新的表面加入 g_LightmapUpdateList:engine/gl_lightmap.cpp:2132-2205。世界表面在构建列表 epilogue 中由 Shader_BuildDynamicLightmaps 检查(遍历 m_DlightSurfaces):engine/gl_rsurf.cpp:4907-4925、engine/gl_rsurf.cpp:2033-2049;画刷实体由 CRender::UpdateBrushModelLightmap 检查:engine/gl_rmain.cpp:778-816。
  • 提交:Begin/EndUpdateLightmaps 是可嵌套的计数;最外层结束时,若有待更新表面且 r_dynamiclighting 打开、r_unloadlightmaps 关闭,就 R_BuildLightmapUpdateList,再 materials->EndUpdateLightmaps():engine/gl_rmain.cpp:769-776、engine/gl_rmain.cpp:818-835。R_BuildLightmapUpdateList 按光照贴图页排序;材质系统有调用队列(队列渲染模式)时把 R_BuildLightMapGuts 排入队列,并为渲染线程复制一份 dlight 状态(双缓冲):engine/gl_lightmap.cpp:2097-2130。
  • 合成:R_BuildLightMapGuts 累加 lightstyle 样本、叠加动态光,最后 UpdateLightmapTextures 调用 materials->UpdateLightmap 写回页内子矩形(凹凸光照写 4 份):engine/gl_lightmap.cpp:1618-1715、engine/gl_lightmap.cpp:1549-1580。若关卡已卸载光照贴图数据,该函数只断言(m_bUnloadedAllLightmaps):engine/gl_lightmap.cpp:1623-1624。

6. 位移面#

  • 构建:DrawDisplacementsInLeaf 以父表面为单位去重,按位移面包围盒做视锥剔除,再按父表面是否半透明加入对应列表:engine/gl_rsurf.cpp:4639-4670。
  • 几何:Shader_DrawDispChain 收集本 sort group 的位移面表面,调用 DispInfo_RenderListWorld 构建图元列表并绘制(r_DrawDisp 关闭或未要求几何时跳过):engine/gl_rsurf.cpp:1964-1995、engine/disp_interface.cpp:1388-1405。
  • 贴花/覆盖层/阴影:DispInfo_RenderListDecalsAndOverlays 依次加覆盖层碎片、阴影、投影光、覆盖层、贴花批处理、投影光贴花和 RTT 阴影:engine/disp_interface.cpp:1410-1460。
  • 客户端 CSM 视图在“极低”质量时用 DRAWWORLDLISTS_DRAW_SKIP_DISPLACEMENTS 跳过位移面:game/client/viewrender.cpp:5193-5196。

7. 静态道具(CStaticPropMgr)#

  • CStaticProp 同时实现 IClientRenderable、ICollideable、IClientModelRenderable;CStaticPropMgr 同时暴露引擎、客户端、服务端三种接口:engine/staticpropmgr.cpp:129、engine/staticpropmgr.cpp:360、engine/staticpropmgr.cpp:459-460。
  • 加载:LevelInit 在未禁用时从 BSP 的 GAMELUMP_STATIC_PROPS 游戏 lump 依次反序列化模型字典、叶子列表、道具:engine/staticpropmgr.cpp:1688-1704、engine/staticpropmgr.cpp:1659-1682。
  • 客户端初始化:CL_FullyConnected 调用 LevelInitClient(注释要求在队列加载结束前):engine/cl_main.cpp:1627-1637。它跳过专用服务器,对不在任何叶子中的道具打印开发者消息,先预计算光照、再按 CPU/GPU 等级更新可见性:engine/staticpropmgr.cpp:1728-1774。
  • 可见性注册:UpdatePropVisibility 按道具的最小/最大 CPU、GPU 等级(PC 才生效)决定是否绘制;可绘制的道具在客户端叶子系统中创建可渲染句柄并加入所在叶子,同时设置“快速反射”“禁用阴影深度”“禁用 CSM”属性:engine/staticpropmgr.cpp:1443-1496。因此静态道具和动态实体一样,经由客户端叶子系统进入每个视图的可渲染列表(见 客户端视图渲染管线 第 4.3 节)。
  • 绘制:DrawStaticProps 在调试模式走慢速路径,否则 pipeline_static_props(默认 1)时走 DrawStaticProps_FastPipeline → modelrender->DrawStaticPropArrayFast,否则逐个 DrawModelExStaticProp:engine/staticpropmgr.cpp:2221-2244、engine/staticpropmgr.cpp:2189-2219、engine/staticpropmgr.cpp:2146-2186。CS:GO 下静态道具 LOD 固定为 0(见 模型、动画与粒子 第 4 节)。

8. 贴花与覆盖层#

  • R_DecalShoot_ 只接受画刷模型(世界或画刷实体)与有效材质:engine/r_decal.cpp:1276-1300;外部入口 R_DecalShoot:engine/r_decal.cpp:1366。静态道具上的贴花另走 CStaticPropMgr::AddDecalToStaticProp:engine/staticpropmgr.cpp:2286。
  • R_DecalAlloc:永久贴花或动态贴花数低于 min(r_decals, g_nMaxDecals) 时找空槽;否则从上次替换处之后找一个动态贴花替换,找不到则(仅告警一次)替换 0 号槽:engine/r_decal.cpp:905-936。
  • 绘制发生在第 4 节的贴花/覆盖层遍:DecalSurfaceDraw 与 OverlayMgr()->RenderOverlays:engine/gl_rsurf.cpp:3519-3531、engine/r_decal.cpp:2547、engine/Overlay.cpp:1009。r_cleardecals 命令可清除世界和模型贴花:engine/gl_rmisc.cpp:162-179。

9. 阴影管理器(shadowmgr)#

引擎 CShadowMgr(IShadowMgr)负责把客户端创建的“阴影/投影光”投射到世界表面上:engine/shadowmgr.cpp:121、engine/shadowmgr.cpp:555-556。

  • 客户端阴影管理器调用 shadowmgr->ProjectShadow(RTT/斑点阴影)与 ProjectFlashlight(投影光):game/client/clientshadowmgr.cpp:3171、game/client/clientshadowmgr.cpp:4056。
  • ProjectShadow 先移除旧的表面/模型关联;阴影被禁用或 r_shadows 0 时到此为止;否则计算包围球与扫掠射线,借用 r_surfacevisframe 保证每个表面只枚举一次:engine/shadowmgr.cpp:1677-1740。ApplyShadowToLeaf 对叶子中的表面做去重、动态阴影开关检查和背面剔除,表示“阴影可能落在此表面”,真正裁剪在渲染时完成(注释):engine/shadowmgr.cpp:1950-1990。
  • 渲染:贴花遍中 AddShadowsOnSurfaceToRenderList → RenderShadows,投影光由 RenderFlashlights 等绘制:engine/gl_rsurf.cpp:3498-3535、engine/shadowmgr.cpp:2138、engine/shadowmgr.cpp:2185、engine/shadowmgr.cpp:3887。
  • CS:GO 客户端强制关闭 RTT 阴影(game/client/clientshadowmgr.cpp:4336-4343),因此该路径在 CS:GO 中主要服务投影光(推断)。CSM 不经过 shadowmgr 的表面投射,而是由 SetCascadedShadowMapping / SetCascadedShadowMappingState 作为渲染上下文状态交给材质系统(game/client/c_env_cascade_light.cpp:1651-1654);着色器如何采样未在本 Wiki 追踪。

10. 天空盒#

  • 2D 天空盒:R_LoadSkys 读取 sv_skyname,未变化则不重载,加载 skybox/<name><后缀> 六面材质:engine/gl_warp.cpp:153-172、engine/gl_warp.cpp:104-116;绘制入口 R_DrawSkyBox:engine/gl_warp.cpp:257,由 Shader_WorldEnd 调用(第 4 节)。
  • 3D 天空盒在客户端实现:参数来自本地玩家的 m_Local.m_skybox3d(地图中的 sky_camera 实体在服务端实现,game/server/SkyCamera.cpp:59):game/client/viewrender.cpp:6758-6778。CSkyboxView::DrawInternal 临时把区域位改成只含天空区域,把视点按 1/scale 缩放并平移到天空原点,然后以天空原点重新 ViewSetupVis:game/client/viewrender.cpp:6788-6830。

约束与边界#

  • 不变量:一个视图的世界列表只在 BuildWorldLists 作业完成后可读,客户端以 SYNC_BUILDWORLD_JOB 等待(game/client/viewrender.cpp:387-400);光照贴图更新只在最外层 EndUpdateLightmaps 提交(engine/gl_rmain.cpp:818-835)。
  • 失败/降级路径:丢失显存时 R_BuildWorldLists 返回空列表、R_DrawWorldLists 不绘制(engine/gl_rsurf.cpp:4725-4736、engine/gl_rsurf.cpp:5118-5119);区域位无效或视点在固体中时不做区域门户裁剪;视点在世界外或 r_novis 时 PVS 全可见;位移面数量越界时报错。
  • 条件编译:DEDICATED 不编译大部分渲染文件;_PS3 有 SPU 世界列表路径(engine/gl_rsurf.cpp:4748-4796);USE_CONVARS 控制调试 ConVar;X360 使用不同的 Z 预通道与阴影深度填充。
  • 未展开:顶视图(R_RenderWorldTopView,用于俯视图/雷达等)、brushbatchrender.cpp 画刷批处理、buildcubemaps.cpp 立方体贴图构建、水面 LOD 与雾体积参数细节、CDispInfo::Render 的细分/LOD、覆盖层碎片裁剪算法、静态道具光照烘焙格式(r_proplightingfromdisk)、Portal 2 涂料(SURFDRAW_PAINTED)路径、叶子可视化调试。
  • PVS 与区域门户数据由离线 vvis/vbsp 生成,见 内容工具链;本页结论均为静态阅读,未运行验证。