引擎世界渲染:BSP 表面、光照贴图与可见性
本页解释 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/SceneEnd | CRender::DrawSceneBegin/End | 检查光照配置变化;场景结束同步(PS3)与叶子可视化调试 | engine/view.cpp:391-399、engine/gl_rmain.cpp:1180-1189 |
render->ViewSetupVisEx | Map_VisSetup | 用一个或多个视点标记 PVS | engine/view.cpp:509-512、engine/gl_rmain.cpp:487-490 |
render->CreateWorldList/BuildWorldLists | AllocWorldRenderList / R_BuildWorldLists | 分配并填充一个视图的世界列表 | engine/view.cpp:406-431、engine/gl_rmain.cpp:1191-1225 |
render->BuildWorldLists_Epilogue | R_BuildWorldLists_Epilogue | 加贴花表面、更新动态光照贴图 | engine/view.cpp:422-425、engine/gl_rmain.cpp:1235-1253 |
render->DrawWorldLists | R_DrawWorldLists | 按 DRAWWORLDLISTS_* 标志绘制列表 | engine/view.cpp:433-436、engine/gl_rmain.cpp:1256-1260 |
render->GetVisibleFogVolume | R_GetVisibleFogVolume | 查询视点所在/可见的水雾体积 | engine/view.cpp:401-404、engine/gl_rsurf.cpp:5047 |
render->SetAreaState | 写 CClientState::m_chAreaBits/m_chAreaPortalBits | 接收服务端下发的区域可见性 | engine/view.cpp:582-590 |
render->DrawBrushModelEx | R_DrawBrushModel | 画刷实体(门、func_*) | engine/view.cpp:296-300、engine/gl_rsurf.cpp:5638-5660 |
render->DrawTranslucentSurfaces | Shader_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
- 光照贴图分配:
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。 - 排序信息与静态网格:
MaterialSystem_CreateSortinfo取回材质系统的排序表、重排并重映射每个表面的排序 ID,然后WorldStaticMeshCreate:engine/gl_matsysiface.cpp:501-550。后者为每个排序 ID 建立静态顶点缓冲,位移面表面被跳过(标记0xFFFF),并告诉阴影管理器材质桶数量:engine/matsys_interface.cpp:1749-1823。 - 位移面:首次渲染该地图时
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。 - 标记水面、
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
- 阴影深度 / SSAO 列表走深度填充专用路径(X360、
r_csm_fast_path、普通三选一)后返回:engine/gl_rsurf.cpp:3334-3350。 r_skybox_draw_last为 0 时先画 2D 天空盒(仅当本列表看到天空或强制全天空):engine/gl_rsurf.cpp:3352-3373。- PC 上
r_fastzreject时先做 Z 填充:engine/gl_rsurf.cpp:3375-3384。 - 从高到低遍历 sort group(注释:先画雾体积,因为只有在雾中才画它,且更近、利于早期 Z 剔除);水面组可加 Z 偏移并启用单遍投影光;每组先画位移面链、再画普通表面链:
engine/gl_rsurf.cpp:3386-3435。 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生成,见 内容工具链;本页结论均为静态阅读,未运行验证。