MetaHookSV plugin that lets other plugins render additional 3D views ("standalone
views") through the engine's own V_RenderView, for example a model preview in a
menu. It does not depend on the Renderer plugin and works with or without it.
The engine calls V_RenderView once per frame from VGui_ViewportPaintBackground,
including at the main menu, where it returns early because cls.state /
cls.signon are not active. StandaloneView hooks both functions. Inside the
paint-background call, before the main view, it:
- forces
cls.state = ca_activeandcls.signon = SIGNONS; - installs an empty brush worldmodel into
cl.worldmodel/r_worldentity.modelunless the client was already active with both set, becauseR_RenderViewrejects a NULL world. While a map loads, the engine setscl.worldmodeland redraws the loading screen beforeR_NewMapsetsr_worldentity.modeland the renderer prepares the world, so a loading world is replaced as well. While the empty world is installed,r_novisis temporarily forced to1to prevent the client's studio visibility test from rejecting preview models; - calls the original
V_RenderViewonce per active view:OnBeforeRenderView, the render pass,OnAfterRenderView; - restores the world,
r_novisand the client state, then lets the main view render as usual. A loaded world'sr_novisis left untouched.
During a view's pass the client export V_CalcRefdef calls only the view's
CalcRefdef, then forces onlyClientDraw = 1 and nextView = 0; the engine
therefore draws neither the world nor its entities. HUD_DrawNormalTriangles /
HUD_DrawTransparentTriangles call only the view's triangle callbacks, where the
view draws its own content with TriAPI, OpenGL or the IStandaloneViewDrawfuncs
passed to the callback. The client's own implementations are not called for
standalone views.
Views render before the main view because each V_RenderView call resets the
sound listener, records the demo view info and writes the refdef back into client
state; the main view then restores all of them. Renderer's SvEngine path also
flushes its final buffer at the start of every V_RenderView call.
Sven Co-op's engine lets the client render its portals at the start of every
V_RenderView. With the Sven Co-op client, StandaloneView also hooks
ClientPortalManager::RenderPortals and ClientPortalManager::ResetAll and skips
both during a standalone pass: the portals belong to the main view, and at the main
menu the client has neither a map nor its OpenGL entry points, which it loads with
the first map. HUD_VidInit resets the portal manager again for every map.
include/Interface/IStandaloneView.h declares IStandaloneView, implemented by
the consumer, IStandaloneViewDrawfuncs, passed to its triangle callbacks, and
IStandaloneViewManager, exposed as StandaloneViewManager_API_003 from
StandaloneView.dll:
auto module = GetModuleHandleA("StandaloneView.dll");
if (module)
{
auto factory = Sys_GetFactory((HINTERFACEMODULE)module);
auto manager = factory ? static_cast<IStandaloneViewManager*>(
factory(STANDALONEVIEW_MANAGER_INTERFACE_VERSION, nullptr)) : nullptr;
if (manager)
manager->RegisterStandaloneView(&g_MyView);
}API_003 added studio pose evaluation, private bone sources and per-draw lighting
directly to IStandaloneViewDrawfuncs, breaking the drawfuncs vtable; the earlier
StandaloneViewManager_API_001 / _API_002 had no consumers and are no longer
exported.
Consumers should treat the plugin as optional and unregister their views before the view objects are destroyed. Usage rules:
- All callbacks run on the main thread, inside the engine's
V_RenderView, whilecls.state/cls.signonare forced active. CalcRefdefsetsvieworg,viewanglesandviewport. Leave thecl_*,simorg,punchangleand similar fields untouched: the engine writes them back into client state. The field of view is not part of the interface.- The pass draws into the viewport of the framebuffer bound at the time, which is
the engine back buffer or Renderer's final buffer. Copy the result out in
OnAfterRenderView. StandaloneView does not clear it afterwards. With Renderer anonlyClientDrawpass is alpha-composited over what the framebuffer already holds, so clear your viewport after copying it if later views or the main menu must not see it. - Views are skipped during demo playback, which replaces the refdef with the recorded view.
- HUD sprites (
SPR_Load) are unavailable until the first map loads, so TriAPI sprite textures cannot be used at the main menu. With Renderer, TriAPI does not sample textures bound withglBindTextureeither; draw there with your own OpenGL resources. IStandaloneViewDrawfuncsdraws one entity with the installed renderer:DrawBrushModelandDrawSpriteModelcall the engine'sR_DrawBrushModel/R_DrawSpriteModel;DrawStudioModelcalls the client'sStudioDrawPlayerfor player entities, with the state of player slotindex - 1, andStudioDrawModelotherwise, withSTUDIO_RENDERonly, so no animation events fire. Each call setscurrententityandr_blendlike the engine's entity lists,1forkRenderNormalandCL_FxBlend / 255otherwise, skips the entity when that is0, and restores both afterwards. A sprite withcurstate.body1-4 is drawn at that attachment of entitycurstate.skin. Glow sprites are not faded by visibility. Only use the pointer until the callback returns; calling it at any other time, or with an entity whose model is missing or of another type, is a fatal error.- In first person Sven Co-op's client skips
StudioDrawPlayerfor the local player entity itself, unless a renderer such as Renderer replaces that path. Draw a copy of the entity to show the local player. - Never call
UnregisterStandaloneViewfrom anIStandaloneViewcallback, for any view: StandaloneView reports it as a fatal error. Unregister, then destroy, outside StandaloneView callbacks, e.g. from the code that closes the owning UI. Views registered from a callback render from the next frame. - List
StandaloneView.dllbefore other plugins inplugins.lst.LoadClientruns in reverse list order, so the first plugin wraps the client exports last and its wrappers run first: other plugins'V_CalcRefdefthen never change a standalone view's refdef.
The studio functions of IStandaloneViewDrawfuncs take an optional bone source and,
for draws, an optional lighting. For example, inside a triangle callback:
standaloneview::StudioLighting lighting;
pDrawfuncs->DrawStudioModel(&previewEntity, &animationSource, &lighting);SetupStudioPose(entity, boneSource)evaluates the pose with flags0: no meshes, visibility culling or animation events. It returns whether evaluation succeeded. The consumer can read the engine'sStudioGetBoneTransformresults immediately afterwards, until the next studio call.DrawStudioModel(entity, boneSource, lighting)renders withSTUDIO_RENDER, neverSTUDIO_EVENTS, as described above.- Pass
nullptrforboneSourceto use the entity's own animation. For a privateMOVETYPE_FOLLOWentity, pass its animation source explicitly: it need not have a slot in the engine entity array. The source pose is evaluated again before every follower, because studio bone merging consumes the last saved bones. An invalid source model or failed source evaluation returnsfalsefromSetupStudioPoseand skipsDrawStudioModel. - A non-null
StudioLightingoverrides ambient light, shade, color and the world-space light-ray direction for that draw only. The direction is normalized; a zero vector falls back to{-1, 0, 0}.enabled = falserequests full ambient lighting. Passnullptrto retain world lighting. If the lighting hook cannot be installed, the plugin warns and the model still renders with world lighting.
The lighting hook survives client interface recaptures but is removed when engine hooks are uninstalled.
With Renderer, a standalone pass with an empty world additionally requires
Renderer to skip its water pass for onlyClientDraws views, and
IStandaloneViewDrawfuncs requires Renderer to route the engine's
R_DrawBrushModel / R_DrawSpriteModel to its own implementations.
Copy StandaloneView.dll to <mod>/metahook/plugins/ and the complete
standaloneview catalog directory to <mod>/metahook/gamedata/standaloneview/. Add
StandaloneView.dll as the first entry of <mod>/metahook/configs/plugins.lst.
MetaHook API 114 or newer is required (QueryGameSymbolStructMember). Sven client
gamedata must include ClientPortalManager.m_bStudioRenderPlayerPending; missing
or incompatible member records fail initialization without a hardcoded fallback.
The shipped catalog covers Half-Life builds 3248, 3266, 3329, 3647, 4554, 6153, 8684, 10210; Sven Co-op builds 8948, 10257; and Cry of Fear build 5936. Unknown binaries or missing required symbols fail with a diagnostic.
Windows, Visual Studio 2022 with MSVC x86, CMake 3.21+, and Python 3.8+ are required.
cmake -S . -B build -A Win32 -DSTANDALONEVIEW_BUILD_TESTS=ON -DCMAKE_INSTALL_PREFIX=install
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure
cmake --install build --config ReleaseRepeat with Debug for the other configuration. METAHOOK_SOURCE_PATH selects a
local, read-only SDK tree; otherwise the SDK is fetched from its main branch.
STANDALONEVIEW_SYNC_GAMEDATA defaults ON and validates all 11 catalog snapshots
before building. With it OFF, a previously generated catalog must be supplied for
installation. Build artifacts and synchronized catalogs stay outside Git.
Standalone Visual Studio F5 deployment is optional:
cmake -S . -B build/launch -A Win32 -DMETAHOOKSV_ENABLE_LAUNCH_GAME=ONThe shared LaunchGame module supports METAHOOKSV_GAME_DIRECTORY,
METAHOOKSV_GAME_APPID, and METAHOOKSV_GAME_MOD; deployment requires an existing
MetaHook installation. See the aggregator documentation.
Regression tests compile the production manager, empty world and hook installation
code against the SDK. They model the engine's V_RenderView and cover render
order, forced and restored client state, the empty world, the paint-background
gate, demo playback, reentrancy, the unregister-during-a-pass contract, export routing,
entity draws through IStandaloneViewDrawfuncs, studio source/follower ordering
and flags in both triangle callbacks, pose failure and engine fallback,
per-draw lighting isolation, lighting-hook failure/retry/recapture/teardown,
r_novis restoration and installation rollback, including MetaHook committing
inline hooks only after LoadEngine / LoadClient return. The full build also
loads the resulting DLL to verify the API_003 factory export, the rejected retired
versions and the manager vtable across the DLL boundary. These tests do not replace in-game verification.
- MetaHookSv issue #942
- Renderer pull request #18, the in-Renderer approach this plugin replaces
SDK components retain their own licensing terms.