Parallax Corrected Cubemaps: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
(Initial (though unfinished) write up of the article)
 
mNo edit summary
 
(26 intermediate revisions by 12 users not shown)
Line 1: Line 1:
== What Is This? ==
{{LanguageBar|title = Parallax Corrected Cubemaps}}


{{todo|This section needs to be filled in.}}
{{For|2={{ent|parallax_obb}}|1=the third-party Source brush entity responsible for setting cubemap boundaries}}
{{For|2={{ent|env_cubemap_box}}|1=the Source 2 point entity responsible for both creating both the cubemap and parallax bounding box}}


[[File:PCC comparison.jpg|400px|thumb|right|<center>'''Comparison of PCC'''</center>]]


Video showcase: [https://www.youtube.com/watch?v=ZH6s1hbwoQQ Parallax Corrected Cubemaps in the Source Engine]
==Overview==
{{src|1.bold}}'s native [[cubemap]] implementation does not allow them to follow the player's perspective. While this is passable in most cases, tying the reflections to the player's view can increase realism, especially when using high-resolution reflections. A method to achieve this is called ''parallax correction''.


== The Code ==
Parallax-corrected cubemaps use a [[parallax_obb|bounding box]] brush to bake their reflection based on a specified area around them, and a custom [[shader]] to make use of it.


Before starting, we'll need this file:
This tutorial is based on the work of Brian Charles. A demonstration video can be found here: {{youtube|ZH6s1hbwoQQ|page=watch|Parallax Corrected Cubemaps in the Source Engine}}.


* [https://gist.github.com/GamerDude27/18bd775198602ddebeee8a9ee1aa8ebf matrixinvert.h]
==The Code==
First, the file
* [[Parallax Corrected Cubemaps/matrixinvert.h|matrixinvert.h]]


should be added to {{path|<nowiki><src code directory>/src/utils/vbsp/</nowiki>}}.


Which you'll put into '''src/utils/vbsp/'''
Next, open the project solution ({{path|everything|sln|icon=file}}) and navigate to {{path|<nowiki>/utils/vbsp/cubemap</nowiki>|cpp}}.


===cubemap.cpp===
Add the following below the <code>SideHasCubemapAndWasntManuallyReferenced( int iSide )</code> function:
<syntaxhighlight lang="cpp">
char *g_pParallaxObbStrs[MAX_MAP_CUBEMAPSAMPLES];
</syntaxhighlight>


Now let's jump right into it, shall we?
Then, modify the <code>CubeMap_InsertSample( ... )</code> function declaration as follows:
<syntaxhighlight lang="cpp">
void Cubemap_InsertSample( const Vector &origin, int size, char *pParallaxObbStr = "" )
</syntaxhighlight>


Go into your '''everything.sln''' solution and open '''cubemap.cpp'''
At the top of the function body, add:
<syntaxhighlight lang="cpp">
g_pParallaxObbStrs[g_nCubemapSamples] = pParallaxObbStr;
</syntaxhighlight>


=== cubemap.cpp ===
Update the <code>PatchEnvmapForMaterialAndDependents( ... )</code> function declaration to:
<syntaxhighlight lang="cpp">
static bool PatchEnvmapForMaterialAndDependents( const char *pMaterialName, const PatchInfo_t &info, const char *pCubemapTexture, const char *pParallaxObbMatrix = "" )
</syntaxhighlight>


Now add this under (not inside) the '''SideHasCubemapAndWasntManuallyReferenced(...)''' function:
Within the <code>if ( pDependentMaterial )</code> statement, change the code to:
<pre>
<syntaxhighlight lang="cpp">
char* g_pParallaxObbStrs[MAX_MAP_CUBEMAPSAMPLES];
bDependentMaterialPatched = PatchEnvmapForMaterialAndDependents( pDependentMaterial, info, pCubemapTexture, pParallaxObbMatrix );
</pre>
</syntaxhighlight>


Then right below, change the '''CubeMap_InsertSample''' function to look like this:
Change
<pre>
<syntaxhighlight lang="cpp">
void Cubemap_InsertSample( const Vector& origin, int size, char* pParallaxObbStr = "" )
MaterialPatchInfo_t pPatchInfo[2];
</pre>
</syntaxhighlight>
And inside that function, at the very top add this:
to:
<pre>
<syntaxhighlight lang="cpp">
g_pParallaxObbStrs[g_nCubemapSamples] = pParallaxObbStr;
MaterialPatchInfo_t pPatchInfo[6];
</pre>
</syntaxhighlight>


Now go down to the '''PatchEnvmapForMaterialAndDependents''' function and change it to look like this:
Above the line
<pre>
<syntaxhighlight lang="cpp">
static bool PatchEnvmapForMaterialAndDependents( const char *pMaterialName, const PatchInfo_t &info, const char *pCubemapTexture, const char *pParallaxObbMatrix = "" )
char pDependentPatchedMaterialName[1024];
</pre>
</syntaxhighlight>
While in the same function, scroll a tiny bit down to '''if (pDependentMaterial)''' and change the line in it to this:
add:
<pre>
<syntaxhighlight lang="cpp">
bDependentMaterialPatched = PatchEnvmapForMaterialAndDependents( pDependentMaterial, info, pCubemapTexture, pParallaxObbMatrix );
// Parallax cubemap matrix
</pre>
CUtlVector<char *> matRowList;
Scroll a tiny bit down again until you find this line:
if ( pParallaxObbMatrix[0] != '\0' )
<pre>
{
MaterialPatchInfo_t pPatchInfo[2];
V_SplitString( pParallaxObbMatrix, ";", matRowList );
</pre>
Then just change 2 to 6, like so:
<pre>
MaterialPatchInfo_t pPatchInfo[6];
</pre>
Do another tiny scroll down, and above this line:
<pre>
char pDependentPatchedMaterialName[1024];
</pre>
Add:
<pre>
// Parallax cubemap matrix
CUtlVector<char *> matRowList;
if ( pParallaxObbMatrix[0] != '\0' )
{
V_SplitString(pParallaxObbMatrix, ";", matRowList);
pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB1";
pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB1";
pPatchInfo[nPatchCount].m_pValue = matRowList[0];
pPatchInfo[nPatchCount].m_pValue = matRowList[0];
++nPatchCount;
++nPatchCount;
pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB2";
pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB2";
pPatchInfo[nPatchCount].m_pValue = matRowList[1];
pPatchInfo[nPatchCount].m_pValue = matRowList[1];
++nPatchCount;
++nPatchCount;
pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB3";
pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB3";
pPatchInfo[nPatchCount].m_pValue = matRowList[2];
pPatchInfo[nPatchCount].m_pValue = matRowList[2];
++nPatchCount;
++nPatchCount;
pPatchInfo[nPatchCount].m_pKey = "$envMapOrigin";
pPatchInfo[nPatchCount].m_pKey = "$envMapOrigin";
pPatchInfo[nPatchCount].m_pValue = matRowList[3];
pPatchInfo[nPatchCount].m_pValue = matRowList[3];
++nPatchCount;
++nPatchCount;
}
}
</pre>
</syntaxhighlight>
At the bottom of the function you'll find this line:
 
<pre>
At the bottom of the function, change:
CreateMaterialPatch( pMaterialName, pPatchedMaterialName, nPatchCount, pPatchInfo, PATCH_INSERT );
<syntaxhighlight lang="cpp">
</pre>
CreateMaterialPatch( pMaterialName, pPatchedMaterialName, nPatchCount, pPatchInfo, PATCH_REPLACE );
Change it to this:
</syntaxhighlight>
<pre>
to:
CreateMaterialPatch( pMaterialName, pPatchedMaterialName, nPatchCount, pPatchInfo, PATCH_INSERT );
<syntaxhighlight lang="cpp">
</pre>
CreateMaterialPatch( pMaterialName, pPatchedMaterialName, nPatchCount, pPatchInfo, PATCH_INSERT );
Then below that add this:
</syntaxhighlight>
<pre>
 
// Clean up parallax stuff
Then add:
matRowList.PurgeAndDeleteElements();
<syntaxhighlight lang="cpp">
</pre>
// Clean up parallax stuff
matRowList.PurgeAndDeleteElements();
</syntaxhighlight>


Scroll down to the '''Cubemap_CreateTexInfo''' function and change it to this:
Update the <code>Cubemap_CreateTexInfo( ... )</code> function declaration to:
<pre>
<syntaxhighlight lang="cpp">
static int Cubemap_CreateTexInfo( int originalTexInfo, int origin[3], int cubemapIndex )
static int Cubemap_CreateTexInfo( int originalTexInfo, int origin[3], int cubemapIndex )
</pre>
</syntaxhighlight>
Scroll a bit down inside that function and find this line:
 
<pre>
After
GeneratePatchedName( "c", info, false, pTextureName, 1024 );
<syntaxhighlight lang="cpp">
</pre>
GeneratePatchedName( "c", info, false, pTextureName, 1024 );
Now below that line add this:
</syntaxhighlight>
<pre>
add:
// Append origin info if this cubemap has a parallax OBB
<syntaxhighlight lang="cpp">
char originAppendedString[1024] = "";
// Append origin info if this cubemap has a parallax OBB
if (g_pParallaxObbStrs[cubemapIndex][0] != '\0')
char originAppendedString[1024] = "";
{
if ( g_pParallaxObbStrs[cubemapIndex][0] != '\0' )
Q_snprintf(originAppendedString, 1024, "%s;[%d %d %d]", g_pParallaxObbStrs[cubemapIndex], origin[0], origin[1], origin[2]);
{
}
Q_snprintf( originAppendedString, 1024, "%s;[%d %d %d]", g_pParallaxObbStrs[cubemapIndex], origin[0], origin[1], origin[2] );
</pre>
}
A bit down you'll see this if statement:
</syntaxhighlight>
<pre>
if ( !PatchEnvmapForMaterialAndDependents( pMaterialName, info, pTextureName ) )
</pre>
Change it to:
<pre>
if ( !PatchEnvmapForMaterialAndDependents( pMaterialName, info, pTextureName, originAppendedString ) )
</pre>


Scroll down until you find the '''Cubemap_FixupBrushSidesMaterials''' function, and inside it find this line:
Change
<pre>
<syntaxhighlight lang="cpp">
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[cubemapID].origin );
if ( !PatchEnvmapForMaterialAndDependents( pMaterialName, info, pTextureName ) )
</pre>
</syntaxhighlight>
Change it to this:
to:
<pre>
<syntaxhighlight lang="cpp">
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[cubemapID].origin, cubemapID );
if ( !PatchEnvmapForMaterialAndDependents( pMaterialName, info, pTextureName, originAppendedString ) )
</pre>
</syntaxhighlight>


Scroll down to the '''Cubemap_AttachDefaultCubemapToSpecularSides''' function, and inside it find this line:
In the <code>Cubemap_FixupBrushSidesMaterials( void )</code> function, change:
<pre>
<syntaxhighlight lang="cpp">
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[iCubemap].origin );
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[cubemapID].origin );
</pre>
</syntaxhighlight>
Change it to this:
to:
<pre>
<syntaxhighlight lang="cpp">
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[iCubemap].origin, iCubemap );
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[cubemapID].origin, cubemapID );
</pre>
</syntaxhighlight>


Now we're done with '''cubemap.cpp''', we can now move over to '''vbsp.h'''
In <code>Cubemap_AttachDefaultCubemapToSpecularSides( void )</code>, change:
<syntaxhighlight lang="cpp">
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[iCubemap].origin );
</syntaxhighlight>
to:
<syntaxhighlight lang="cpp">
pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[iCubemap].origin, iCubemap );
</syntaxhighlight>


=== vbsp.h ===
This completes the changes for {{path|cubemap|cpp}}.


Now find this line:
===vbsp.h===
<pre>
Change the declaration:
<syntaxhighlight lang="cpp">
void Cubemap_InsertSample( const Vector& origin, int size );
void Cubemap_InsertSample( const Vector& origin, int size );
</pre>
</syntaxhighlight>
And change it to this:
to:
<pre>
<syntaxhighlight lang="cpp">
void Cubemap_InsertSample( const Vector& origin, int size, char* pParallaxObbStr );
void Cubemap_InsertSample( const Vector &origin, int size, char *pParallaxObbStr );
</pre>
</syntaxhighlight>
And above that add this:
<pre>
extern char* g_pParallaxObbStrs[MAX_MAP_CUBEMAPSAMPLES];
</pre>


Now we're done with '''vbsp.h''', now let's go into '''map.cpp'''
Above that, add:
<syntaxhighlight lang="cpp">
extern char *g_pParallaxObbStrs[MAX_MAP_CUBEMAPSAMPLES];
</syntaxhighlight>


=== map.cpp ===
This completes the changes for {{path|vbsp|h}}.


At the very top, add this include:
===map.cpp===
<pre>
At the top, add:
<syntaxhighlight lang="cpp">
#include "matrixinvert.h"
#include "matrixinvert.h"
</pre>
</syntaxhighlight>
Then scroll down to the '''LoadEntityCallback''' function, then inside it find this line:
<pre>
const char *pSideListStr = ValueForKey( mapent, "sides" );
</pre>
Now below that line, add:
<pre>
char *pParallaxObbStr = ValueForKey( mapent, "parallaxobb" );
</pre>
Then a tiny bit down find this line:
<pre>
Cubemap_InsertSample( mapent->origin, size );
</pre>
And change it to this:
<pre>
Cubemap_InsertSample( mapent->origin, size, pParallaxObbStr);
</pre>
Now below the parent if statement we're in, add this:
<pre>
//
// parallax_obb brushes are removed after the transformation matrix is found and saved into
// the entity's data (ent will be removed after data transferred to patched materials)
//
if (!strcmp("parallax_obb", pClassName))
{
matrix3x4_t obbMatrix, invObbMatrix;
SetIdentityMatrix(obbMatrix);
SetIdentityMatrix(invObbMatrix);


// Get corner and its 3 edges (scaled, local x, y, and z axes)
In the <code>LoadEntityCallback( ... )</code> function, after:
mapbrush_t *brush = &mapbrushes[mapent->firstbrush];
<syntaxhighlight lang="cpp">
Vector corner, x, y, z;
const char *pSideListStr = ValueForKey( mapent, "sides" );
</syntaxhighlight>
add:
<syntaxhighlight lang="cpp">
char *pParallaxObbStr = ValueForKey( mapent, "parallaxobb" );
</syntaxhighlight>


// Find first valid winding (with these whiles, if not enough valid windings then identity matrix is passed through to vmts)
Replace:
int i = 0;
<syntaxhighlight lang="cpp">
while (i < brush->numsides)
Cubemap_InsertSample( mapent->origin, size );
</syntaxhighlight>
with:
<syntaxhighlight lang="cpp">
Cubemap_InsertSample( mapent->origin, size, pParallaxObbStr );
</syntaxhighlight>
 
After the closing bracket of the <code>if</code> statement, add:
<syntaxhighlight lang="cpp">
//
// parallax_obb brushes are removed after the transformation matrix is found and saved into
// the entity's data (ent will be removed after data transferred to patched materials)
//
if ( !strcmp( "parallax_obb", pClassName ) )
{
{
winding_t* wind = brush->original_sides[i].winding;
matrix3x4_t obbMatrix, invObbMatrix;
if (!wind)
SetIdentityMatrix( obbMatrix );
SetIdentityMatrix( invObbMatrix );
 
// Get corner and its 3 edges (scaled, local x, y, and z axes)
mapbrush_t *brush = &mapbrushes[mapent->firstbrush];
Vector corner, x, y, z;
 
// Find first valid winding (with these whiles, if not enough valid windings then identity matrix is passed through to VMTs)
int i = 0;
while ( i < brush->numsides )
{
winding_t *wind = brush->original_sides[i].winding;
if ( !wind )
{
i++;
continue;
}
 
corner = wind->p[0];
y = wind->p[1] - corner;
z = wind->p[3] - corner;
x = CrossProduct( y, z ).Normalized();
 
i++;
break;
}
 
// Skip second valid winding (opposite face from first, unusable for finding Z's length)
while ( i < brush->numsides )
{
{
winding_t *wind = brush->original_sides[i].winding;
if ( !wind )
{
i++;
continue;
}
i++;
i++;
continue;
break;
}
}


corner = wind->p[0];
// Find third valid winding
y = wind->p[1] - corner;
while ( i < brush->numsides )
z = wind->p[3] - corner;
{
x = CrossProduct(y, z).Normalized();
winding_t *wind = brush->original_sides[i].winding;
if ( !wind )
{
i++;
continue;
}
 
// Find length of X
// Start with diagonal, then scale X by the projection of diag onto X
Vector diag = wind->p[0] - wind->p[2];
x *= abs( DotProduct( diag, x ) );
 
// Build transformation matrix (what is needed to turn a [0,0,0] - [1,1,1] cube into this brush)
MatrixSetColumn( x, 0, obbMatrix );
MatrixSetColumn( y, 1, obbMatrix );
MatrixSetColumn( z, 2, obbMatrix );
MatrixSetColumn( corner, 3, obbMatrix );


i++;
// Find inverse (we need the world to local matrix, "transformationmatrix" is kind of a misnomer)
break;
MatrixInversion( obbMatrix, invObbMatrix );
break;
}
char szMatrix[1024];
Q_snprintf( szMatrix, 1024, "[%f %f %f %f];[%f %f %f %f];[%f %f %f %f]", invObbMatrix[0][0], invObbMatrix[0][1], invObbMatrix[0][2], invObbMatrix[0][3], invObbMatrix[1][0], invObbMatrix[1][1], invObbMatrix[1][2], invObbMatrix[1][3], invObbMatrix[2][0], invObbMatrix[2][1], invObbMatrix[2][2], invObbMatrix[2][3] );
SetKeyValue( mapent, "transformationmatrix", szMatrix );
 
return ( ChunkFile_Ok );
}
}
</syntaxhighlight>


// Skip second valid winding (opposite face from first, unusable for finding Z's length)
In the <code>LoadMapFile( ... )</code> function, after:
while (i < brush->numsides)
<syntaxhighlight lang="cpp">
g_LoadingMap->CheckForInstances( pszFileName );
</syntaxhighlight>
add:
<syntaxhighlight lang="cpp">
if ( g_MainMap == g_LoadingMap )
{
{
winding_t* wind = brush->original_sides[i].winding;
// Fill out parallax obb matrix array
if (!wind)
for ( int i = 0; i < g_nCubemapSamples; i++ )
{
if ( g_pParallaxObbStrs[i][0] != '\0' )
{
entity_t *obbEnt = EntityByName( g_pParallaxObbStrs[i] );
g_pParallaxObbStrs[i] = ValueForKey( obbEnt, "transformationmatrix" );
}
}
// Remove parallax_obb entities (in a nice slow linear search)
for ( int i = 0; i < g_MainMap->num_entities; i++ )
{
{
i++;
entity_t *mapent = &g_MainMap->entities[i];
continue;
const char *pClassName = ValueForKey( mapent, "classname" );
if ( !strcmp( "parallax_obb", pClassName ) )
{
mapent->numbrushes = 0;
mapent->epairs = NULL;
}
}
}
i++;
break;
}
}
</syntaxhighlight>
This completes the changes for {{path|map|cpp}}.
The {{vbsp|4}} project should now compile without errors. If errors occur, review the changes for accuracy.
Once {{code|vbsp.exe}} is compiled, move it to the game's {{path|bin/}} folder (not the mod's {{code|bin/}}; the game's - typically the same one where [[Hammer]] is located).
{{warning|[[Steam]] often replaces custom vbsp.exe with its original version whenever it verifies the file integrity of the game.{{workaround|To prevent the loss of the custom vbsp.exe, rename it, then point to it in [[Hammer]]'s game configuration.}}}}
==The Shaders==
The Parallax Corrected Cubemaps that are now patched into materials require a custom [[LightmappedGeneric]] shader for proper display.
To edit shaders, open the {{path|everything|sln|icon=file}} file.
{{warning|This section is the lengthiest and most tedious, due to the need to compile a custom '''LightmappedGeneric''' shader, which can take upwards of 30 minutes per compile, even on high-end computers.}}


// Find third valid winding
{{note|It is recommended to follow the [[Shader Authoring]] tutorial to become familiar with shader compilation. This tutorial assumes a basic understanding of that process.}}
while (i < brush->numsides)
{
winding_t* wind = brush->original_sides[i].winding;
if (!wind)
{
i++;
continue;
}


// Find length of x
===Shader C++ Files===
// Start with diagonal, then scale x by the projection of diag onto x
First, update the '''LightmappedGeneric''' shader to recognize the Parallax Corrected bounding boxes stored in the material.
Vector diag = wind->p[0] - wind->p[2];
x *= abs(DotProduct(diag, x));


// Build transformation matrix (what is needed to turn a [0,0,0] - [1,1,1] cube into this brush)
====lightmappedgeneric_dx9_helper.h====
MatrixSetColumn(x, 0, obbMatrix);
Add the following lines to the end of the <code>LightmappedGeneric_DX9_Vars_t</code> struct (after the <code>int m_nOutlineEnd1;</code> member):
MatrixSetColumn(y, 1, obbMatrix);
<syntaxhighlight lang="cpp">
MatrixSetColumn(z, 2, obbMatrix);
// Parallax cubemaps
MatrixSetColumn(corner, 3, obbMatrix);
int m_nEnvmapParallaxObb1;
int m_nEnvmapParallaxObb2;
int m_nEnvmapParallaxObb3;
int m_nEnvmapOrigin;
</syntaxhighlight>


//find inverse (we need the world to local matrix, "transformationmatrix" is kind of a misnomer)
====lightmappedgeneric_dx9.cpp====
MatrixInversion(obbMatrix, invObbMatrix);
Add the following lines inside the
break;
<syntaxhighlight lang="cpp">
}
BEGIN_SHADER_PARAMS
</syntaxhighlight>
char szMatrix[1024];
block, after the  
Q_snprintf(szMatrix, 1024, "[%f %f %f %f];[%f %f %f %f];[%f %f %f %f]", invObbMatrix[0][0], invObbMatrix[0][1], invObbMatrix[0][2], invObbMatrix[0][3], invObbMatrix[1][0], invObbMatrix[1][1], invObbMatrix[1][2], invObbMatrix[1][3], invObbMatrix[2][0], invObbMatrix[2][1], invObbMatrix[2][2], invObbMatrix[2][3]);
<syntaxhighlight lang="cpp">
SetKeyValue(mapent, "transformationmatrix", szMatrix);
SHADER_PARAM( OUTLINEEND1, SHADER_PARAM_TYPE_FLOAT, "0.0", "outer end value for outline")
</syntaxhighlight>
line:
<syntaxhighlight lang="cpp">
// Parallax cubemaps
SHADER_PARAM( ENVMAPPARALLAXOBB1, SHADER_PARAM_TYPE_VEC4, "[1 0 0 0]", "The first line of the parallax correction OBB matrix" )
SHADER_PARAM( ENVMAPPARALLAXOBB2, SHADER_PARAM_TYPE_VEC4, "[0 1 0 0]", "The second line of the parallax correction OBB matrix" )
SHADER_PARAM( ENVMAPPARALLAXOBB3, SHADER_PARAM_TYPE_VEC4, "[0 0 1 0]", "The third line of the parallax correction OBB matrix" )
SHADER_PARAM( ENVMAPORIGIN, SHADER_PARAM_TYPE_VEC3, "[0 0 0]", "The world space position of the env_cubemap being corrected" )
</syntaxhighlight>


return (ChunkFile_Ok);
In the same file, inside the
}
<syntaxhighlight lang="cpp">
</pre>
void SetupVars( LightmappedGeneric_DX9_Vars_t& info )
</syntaxhighlight>
function, add the following lines at the end (after <code>info.m_nOutlineEnd1 = OUTLINEEND1;</code>):
<syntaxhighlight lang="cpp">
// Parallax cubemaps
info.m_nEnvmapParallaxObb1 = ENVMAPPARALLAXOBB1;
info.m_nEnvmapParallaxObb2 = ENVMAPPARALLAXOBB2;
info.m_nEnvmapParallaxObb3 = ENVMAPPARALLAXOBB3;
info.m_nEnvmapOrigin = ENVMAPORIGIN;
</syntaxhighlight>


Now scroll down to the '''LoadMapFile''' function, inside it scroll down until you find this line:
====lightmappedgeneric_dx9_helper.cpp====
<pre>
Inside
if ((eResult == ChunkFile_Ok) || (eResult == ChunkFile_EOF))
<syntaxhighlight lang="cpp">
</pre>
void InitParamsLightmappedGeneric_DX9( CBaseVSShader *pShader, IMaterialVar** params, const char *pMaterialName, LightmappedGeneric_DX9_Vars_t &info )
Now above that line, add this:
</syntaxhighlight>
<pre>
add the following lines after <code>InitFloatParam( info.m_nOutlineAlpha, params, 1.0 );</code>:
// Fill out parallax obb matrix array
<syntaxhighlight lang="cpp">
for (int i = 0; i < g_nCubemapSamples; i++)  
// Parallax cubemaps
{
// Cubemap parallax correction requires all 4 lines (if the 2nd, 3rd, or 4th are undef, undef the first one (checking done on first var)
if (g_pParallaxObbStrs[i][0] != '\0')
if ( !( params[info.m_nEnvmapParallaxObb2]->IsDefined() && params[info.m_nEnvmapParallaxObb3]->IsDefined() && params[info.m_nEnvmapOrigin]->IsDefined() ) )
{
{
entity_t* obbEnt = EntityByName(g_pParallaxObbStrs[i]);
params[info.m_nEnvmapParallaxObb1]->SetIntValue( 0 );
g_pParallaxObbStrs[i] = ValueForKey(obbEnt, "transformationmatrix");
}
}
}
</syntaxhighlight>
 
In the
<syntaxhighlight lang="cpp">
void DrawLightmappedGeneric_DX9_Internal( ... )
</syntaxhighlight>
function, after
<syntaxhighlight lang="cpp">
bool hasNormalMapAlphaEnvmapMask = IS_FLAG_SET( MATERIAL_VAR_NORMALMAPALPHAENVMAPMASK );
</syntaxhighlight>
add:
<syntaxhighlight lang="cpp">
// Parallax cubemaps
bool hasParallaxCorrection = params[info.m_nEnvmapParallaxObb1]->GetType() == MATERIAL_VAR_TYPE_VECTOR;
</syntaxhighlight>
 
Within the <code>if ( g_pHardwareConfig->SupportsPixelShaders_2_b() )</code> block, add before
<syntaxhighlight lang="cpp">
SET_STATIC_PIXEL_SHADER( lightmappedgeneric_ps20b );
</syntaxhighlight>
the following:
<syntaxhighlight lang="cpp">
// Parallax cubemaps enabled for 2_0b and onwards
SET_STATIC_PIXEL_SHADER_COMBO( PARALLAXCORRECT, hasParallaxCorrection );
</syntaxhighlight>
 
In the <code>else</code> statement for the above, before <code>SET_STATIC_PIXEL_SHADER( lightmappedgeneric_ps20 );</code>, add:
<syntaxhighlight lang="cpp">
// Parallax cubemaps
SET_STATIC_PIXEL_SHADER_COMBO( PARALLAXCORRECT, 0 ); // No parallax cubemaps with ps_2_0 :(
</syntaxhighlight>


// Remove parallax_obb entities (in a nice slow linear search)
Before <code>pContextData->m_SemiStaticCmdsOut.End();</code>, add:
for (int i = 0; i < g_MainMap->num_entities; i++)
<syntaxhighlight lang="cpp">
{
// Parallax cubemaps
entity_t* mapent = &g_MainMap->entities[i];
if ( hasParallaxCorrection )
const char *pClassName = ValueForKey( mapent, "classname" );
if ( !strcmp( "parallax_obb", pClassName ) )
{
{
mapent->numbrushes = 0;
pContextData->m_SemiStaticCmdsOut.SetPixelShaderConstant( 21, params[info.m_nEnvmapOrigin]->GetVecValue() );
mapent->epairs = NULL;
 
float *vecs[3];
vecs[0] = const_cast<float *>( params[info.m_nEnvmapParallaxObb1]->GetVecValue() );
vecs[1] = const_cast<float *>( params[info.m_nEnvmapParallaxObb2]->GetVecValue() );
vecs[2] = const_cast<float *>( params[info.m_nEnvmapParallaxObb3]->GetVecValue() );
float matrix[4][4];
for ( int i = 0; i < 3; i++ )
{
for ( int j = 0; j < 4; j++ )
{
matrix[i][j] = vecs[i][j];
}
}
matrix[3][0] = matrix[3][1] = matrix[3][2] = 0;
matrix[3][3] = 1;
pContextData->m_SemiStaticCmdsOut.SetPixelShaderConstant( 22, &matrix[0][0], 4 );
}
}
}
</syntaxhighlight>
</pre>
 
===The Shader Files===
====lightmappedgeneric_ps2_3_x.h====
Below
<syntaxhighlight lang="cpp">
const float4 g_ShadowTweaks : register( c19 );
</syntaxhighlight>
add:
<syntaxhighlight lang="cpp">
// Parallax cubemaps
#if ( PARALLAXCORRECT )
const float3 g_CubemapPos : register( c21 );
const float4x4 g_ObbMatrix : register( c22 ); // Through c25
#endif
</syntaxhighlight>
 
Within the cubemap processing block, after <code>fresnel = fresnel * g_OneMinusFresnelReflection + g_FresnelReflection;</code>, add:
<syntaxhighlight lang="cpp">
// Parallax correction (2_0b and beyond)
// Adapted from http://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/
#if !( defined( SHADER_MODEL_PS_1_1 ) || defined( SHADER_MODEL_PS_1_4 ) || defined( SHADER_MODEL_PS_2_0 ) )
#if ( PARALLAXCORRECT )
float3 worldPos = i.worldPos_projPosZ.xyz;
float3 positionLS = mul( float4( worldPos, 1 ), g_ObbMatrix );
float3 rayLS = mul( reflectVect, (float3x3)g_ObbMatrix );
 
float3 firstPlaneIntersect = ( float3( 1.0f, 1.0f, 1.0f ) - positionLS ) / rayLS;
float3 secondPlaneIntersect = ( -positionLS ) / rayLS;
float3 furthestPlane = max( firstPlaneIntersect, secondPlaneIntersect );
float distance = min( furthestPlane.x, min( furthestPlane.y, furthestPlane.z ) );


And that should be it! Try compiling the VBSP project, it should compile without any errors.
// Use distance in WS directly to recover intersection
float3 intersectPositionWS = worldPos + reflectVect * distance;
reflectVect = intersectPositionWS - g_CubemapPos;
#endif
#endif
</syntaxhighlight>


Now all that's left is moving the freshly compiled '''vbsp.exe''' file to your '''bin''' folder
====lightmappedgeneric_ps2x.fxc====
After <code>// STATIC: "FLASHLIGHT" "0..1" [ps20b] [XBOX]</code>, add:
<syntaxhighlight lang="text">
// STATIC: "PARALLAXCORRECT" "0..1"
</syntaxhighlight>


== The Shaders ==
After <code>// SKIP ($DETAIL_BLEND_MODE == 11 ) && ($BUMPMAP != 0 )</code>, add:
<syntaxhighlight lang="text">
// SKIP: $PARALLAXCORRECT && !$CUBEMAP
// SKIP: $PARALLAXCORRECT [ps20]
</syntaxhighlight>


{{todo|Coming soon (1.10.2018)}}
===Compile the Shader(s)===
Ensure the following shaders are listed in {{path|stdshader_dx9_20b|txt}} (or the custom game file):
<syntaxhighlight lang="text">
lightmappedgeneric_ps2x.fxc
lightmappedgeneric_ps11.fxc
lightmappedgeneric_vs20.fxc
</syntaxhighlight>


== What Next? ==
Compile the shaders. This process may take a significant amount of time. Upon completion, the appropriate {{path|lightmappedgeneric_*|inc|icon=file}} and {{path|lightmappedgeneric_*|vcs|icon=file}} files will be generated. Copy these to the game's {{path|shaders/fxc/}} folder.


=== The FGD File ===
With the new '''[[INC|.inc]]''' files, compile the {{path|game_shader_dx9|sln|icon=file}} project. The resulting {{path|game_shader_dx9|dll|icon=file}} should be copied to the game's {{path|bin/}} folder, alongside the server and client DLLs.


'''parallaxcubes.fgd:'''
==What Next?==
<pre>
 
@include "YOUR FGD HERE. THIS ONE WILL OVERRIDE THE REGULAR ENV_CUBEMAP ENTITY AND ALSO ADD THE PARALLAX_OBB ENTITY. ONLY INCLUDE THIS ONE IN HAMMER."
===The FGD File===
{{CodeBlock|src=parallaxcubes.fgd
|<nowiki>@include "YOUR FGD HERE. THIS ONE WILL OVERRIDE THE REGULAR ENV_CUBEMAP ENTITY AND ALSO ADD THE PARALLAX_OBB ENTITY. ONLY INCLUDE THIS ONE IN HAMMER."


@PointClass color(0 0 255) sidelist(sides) iconsprite("editor/env_cubemap.vmt") = env_cubemap :  
@PointClass color(0 0 255) sidelist(sides) iconsprite("editor/env_cubemap.vmt") = env_cubemap :  
Line 329: Line 523:
targetname(target_source) : "Name" : : "The name that other entities refer to this entity by."
targetname(target_source) : "Name" : : "The name that other entities refer to this entity by."
]
]
</pre>
</nowiki>}}
 
=== (Optional) Test Map ===
 
{{todo|Coming soon (1.10.2018)}}
 
== Conclusion ==


And that's it, try launching your mod to see if it works!
===(Optional) Test Map===
Instructions for creating Parallax Corrected Cubemaps can be found in the [[{{PAGENAME}}/Creation]] article.


This is currently only tested on the SP branch of Source SDK 2013. The MP branch and Source SDK 2007 remains untested as of writing this article.
{{todo|Test Map coming soon}}


[[Category:Programming]]
==Conclusion==
[[Category:Shaders]]
At this point, the mod should be ready to launch with parallax-corrected cubemaps enabled.
[[Category:Free source code]]

Latest revision as of 11:51, 14 October 2025

English (en)Français (fr)Русский (ru)Translate (Translate)
For the third-party Source brush entity responsible for setting cubemap boundaries, see parallax_obb.
For the Source 2 point entity responsible for both creating both the cubemap and parallax bounding box, see env_cubemap_box.
Comparison of PCC

Overview

Source's native cubemap implementation does not allow them to follow the player's perspective. While this is passable in most cases, tying the reflections to the player's view can increase realism, especially when using high-resolution reflections. A method to achieve this is called parallax correction.

Parallax-corrected cubemaps use a bounding box brush to bake their reflection based on a specified area around them, and a custom shader to make use of it.

This tutorial is based on the work of Brian Charles. A demonstration video can be found here: YouTube logo Parallax Corrected Cubemaps in the Source Engine.

The Code

First, the file

should be added to 🖿<src code directory>/src/utils/vbsp/.

Next, open the project solution (Fileeverything.sln) and navigate to 🖿/utils/vbsp/cubemap.cpp.

cubemap.cpp

Add the following below the SideHasCubemapAndWasntManuallyReferenced( int iSide ) function:

char *g_pParallaxObbStrs[MAX_MAP_CUBEMAPSAMPLES];

Then, modify the CubeMap_InsertSample( ... ) function declaration as follows:

void Cubemap_InsertSample( const Vector &origin, int size, char *pParallaxObbStr = "" )

At the top of the function body, add:

g_pParallaxObbStrs[g_nCubemapSamples] = pParallaxObbStr;

Update the PatchEnvmapForMaterialAndDependents( ... ) function declaration to:

static bool PatchEnvmapForMaterialAndDependents( const char *pMaterialName, const PatchInfo_t &info, const char *pCubemapTexture, const char *pParallaxObbMatrix = "" )

Within the if ( pDependentMaterial ) statement, change the code to:

	bDependentMaterialPatched = PatchEnvmapForMaterialAndDependents( pDependentMaterial, info, pCubemapTexture, pParallaxObbMatrix );

Change

	MaterialPatchInfo_t pPatchInfo[2];

to:

	MaterialPatchInfo_t pPatchInfo[6];

Above the line

	char pDependentPatchedMaterialName[1024];

add:

	// Parallax cubemap matrix
	CUtlVector<char *> matRowList;
	if ( pParallaxObbMatrix[0] != '\0' )
	{
		V_SplitString( pParallaxObbMatrix, ";", matRowList );
	
		pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB1";
		pPatchInfo[nPatchCount].m_pValue = matRowList[0];
		++nPatchCount;
		pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB2";
		pPatchInfo[nPatchCount].m_pValue = matRowList[1];
		++nPatchCount;
		pPatchInfo[nPatchCount].m_pKey = "$envMapParallaxOBB3";
		pPatchInfo[nPatchCount].m_pValue = matRowList[2];
		++nPatchCount;
		pPatchInfo[nPatchCount].m_pKey = "$envMapOrigin";
		pPatchInfo[nPatchCount].m_pValue = matRowList[3];
		++nPatchCount;
	}

At the bottom of the function, change:

	CreateMaterialPatch( pMaterialName, pPatchedMaterialName, nPatchCount, pPatchInfo, PATCH_REPLACE );

to:

	CreateMaterialPatch( pMaterialName, pPatchedMaterialName, nPatchCount, pPatchInfo, PATCH_INSERT );

Then add:

	// Clean up parallax stuff
	matRowList.PurgeAndDeleteElements();

Update the Cubemap_CreateTexInfo( ... ) function declaration to:

static int Cubemap_CreateTexInfo( int originalTexInfo, int origin[3], int cubemapIndex )

After

	GeneratePatchedName( "c", info, false, pTextureName, 1024 );

add:

	// Append origin info if this cubemap has a parallax OBB
	char originAppendedString[1024] = "";
	if ( g_pParallaxObbStrs[cubemapIndex][0] != '\0' )
	{
		Q_snprintf( originAppendedString, 1024, "%s;[%d %d %d]", g_pParallaxObbStrs[cubemapIndex], origin[0], origin[1], origin[2] );
	}

Change

	if ( !PatchEnvmapForMaterialAndDependents( pMaterialName, info, pTextureName ) )

to:

	if ( !PatchEnvmapForMaterialAndDependents( pMaterialName, info, pTextureName, originAppendedString ) )

In the Cubemap_FixupBrushSidesMaterials( void ) function, change:

	pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[cubemapID].origin );

to:

	pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[cubemapID].origin, cubemapID );

In Cubemap_AttachDefaultCubemapToSpecularSides( void ), change:

	pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[iCubemap].origin );

to:

	pSide->texinfo = Cubemap_CreateTexInfo( pSide->texinfo, g_CubemapSamples[iCubemap].origin, iCubemap );

This completes the changes for 🖿cubemap.cpp.

vbsp.h

Change the declaration:

void Cubemap_InsertSample( const Vector& origin, int size );

to:

void Cubemap_InsertSample( const Vector &origin, int size, char *pParallaxObbStr );

Above that, add:

extern char *g_pParallaxObbStrs[MAX_MAP_CUBEMAPSAMPLES];

This completes the changes for 🖿vbsp.h.

map.cpp

At the top, add:

#include "matrixinvert.h"

In the LoadEntityCallback( ... ) function, after:

	const char *pSideListStr = ValueForKey( mapent, "sides" );

add:

	char *pParallaxObbStr = ValueForKey( mapent, "parallaxobb" );

Replace:

	Cubemap_InsertSample( mapent->origin, size );

with:

	Cubemap_InsertSample( mapent->origin, size, pParallaxObbStr );

After the closing bracket of the if statement, add:

	//
	// parallax_obb brushes are removed after the transformation matrix is found and saved into 
	// the entity's data (ent will be removed after data transferred to patched materials)
	//
	if ( !strcmp( "parallax_obb", pClassName ) )
	{
		matrix3x4_t obbMatrix, invObbMatrix;
		SetIdentityMatrix( obbMatrix );
		SetIdentityMatrix( invObbMatrix );

		// Get corner and its 3 edges (scaled, local x, y, and z axes)
		mapbrush_t *brush = &mapbrushes[mapent->firstbrush];
		Vector corner, x, y, z;

		// Find first valid winding (with these whiles, if not enough valid windings then identity matrix is passed through to VMTs)
		int i = 0;
		while ( i < brush->numsides )
		{
			winding_t *wind = brush->original_sides[i].winding;
			if ( !wind )
			{
				i++;
				continue;
			}

			corner = wind->p[0];
			y = wind->p[1] - corner;
			z = wind->p[3] - corner;
			x = CrossProduct( y, z ).Normalized();

			i++;
			break;
		}

		// Skip second valid winding (opposite face from first, unusable for finding Z's length)
		while ( i < brush->numsides )
		{
			winding_t *wind = brush->original_sides[i].winding;
			if ( !wind )
			{
				i++;
				continue;
			}
			i++;
			break;
		}

		// Find third valid winding
		while ( i < brush->numsides )
		{
			winding_t *wind = brush->original_sides[i].winding;
			if ( !wind )
			{
				i++;
				continue;
			}

			// Find length of X
			// Start with diagonal, then scale X by the projection of diag onto X
			Vector diag = wind->p[0] - wind->p[2];
			x *= abs( DotProduct( diag, x ) );

			// Build transformation matrix (what is needed to turn a [0,0,0] - [1,1,1] cube into this brush)
			MatrixSetColumn( x, 0, obbMatrix );
			MatrixSetColumn( y, 1, obbMatrix );
			MatrixSetColumn( z, 2, obbMatrix );
			MatrixSetColumn( corner, 3, obbMatrix );

			// Find inverse (we need the world to local matrix, "transformationmatrix" is kind of a misnomer)
			MatrixInversion( obbMatrix, invObbMatrix );
			break;
		}
			
		char szMatrix[1024];
		Q_snprintf( szMatrix, 1024, "[%f %f %f %f];[%f %f %f %f];[%f %f %f %f]", invObbMatrix[0][0], invObbMatrix[0][1], invObbMatrix[0][2], invObbMatrix[0][3], invObbMatrix[1][0], invObbMatrix[1][1], invObbMatrix[1][2], invObbMatrix[1][3], invObbMatrix[2][0], invObbMatrix[2][1], invObbMatrix[2][2], invObbMatrix[2][3] );
		SetKeyValue( mapent, "transformationmatrix", szMatrix );

		return ( ChunkFile_Ok );
	}

In the LoadMapFile( ... ) function, after:

	g_LoadingMap->CheckForInstances( pszFileName );

add:

	if ( g_MainMap == g_LoadingMap )
	{
		// Fill out parallax obb matrix array
		for ( int i = 0; i < g_nCubemapSamples; i++ )
		{
			if ( g_pParallaxObbStrs[i][0] != '\0' )
			{
				entity_t *obbEnt = EntityByName( g_pParallaxObbStrs[i] );
				g_pParallaxObbStrs[i] = ValueForKey( obbEnt, "transformationmatrix" );
			}
		}
		
		// Remove parallax_obb entities (in a nice slow linear search)
		for ( int i = 0; i < g_MainMap->num_entities; i++ )
		{
			entity_t *mapent = &g_MainMap->entities[i];
			const char *pClassName = ValueForKey( mapent, "classname" );
			if ( !strcmp( "parallax_obb", pClassName ) )
			{
				mapent->numbrushes = 0;
				mapent->epairs = NULL;
			}
		}
	}

This completes the changes for 🖿map.cpp.

The VBSP VBSP project should now compile without errors. If errors occur, review the changes for accuracy.

Once vbsp.exe is compiled, move it to the game's 🖿bin/ folder (not the mod's bin/; the game's - typically the same one where Hammer is located).

Warning.pngWarning:Steam often replaces custom vbsp.exe with its original version whenever it verifies the file integrity of the game.
PlacementTip.pngWorkaround:To prevent the loss of the custom vbsp.exe, rename it, then point to it in Hammer's game configuration.

The Shaders

The Parallax Corrected Cubemaps that are now patched into materials require a custom LightmappedGeneric shader for proper display.

To edit shaders, open the Fileeverything.sln file.

Warning.pngWarning:This section is the lengthiest and most tedious, due to the need to compile a custom LightmappedGeneric shader, which can take upwards of 30 minutes per compile, even on high-end computers.
Note.pngNote:It is recommended to follow the Shader Authoring tutorial to become familiar with shader compilation. This tutorial assumes a basic understanding of that process.

Shader C++ Files

First, update the LightmappedGeneric shader to recognize the Parallax Corrected bounding boxes stored in the material.

lightmappedgeneric_dx9_helper.h

Add the following lines to the end of the LightmappedGeneric_DX9_Vars_t struct (after the int m_nOutlineEnd1; member):

	// Parallax cubemaps
	int m_nEnvmapParallaxObb1;
	int m_nEnvmapParallaxObb2;
	int m_nEnvmapParallaxObb3;
	int m_nEnvmapOrigin;

lightmappedgeneric_dx9.cpp

Add the following lines inside the

BEGIN_SHADER_PARAMS

block, after the

	SHADER_PARAM( OUTLINEEND1, SHADER_PARAM_TYPE_FLOAT, "0.0", "outer end value for outline")

line:

	// Parallax cubemaps
	SHADER_PARAM( ENVMAPPARALLAXOBB1, SHADER_PARAM_TYPE_VEC4, "[1 0 0 0]", "The first line of the parallax correction OBB matrix" )
	SHADER_PARAM( ENVMAPPARALLAXOBB2, SHADER_PARAM_TYPE_VEC4, "[0 1 0 0]", "The second line of the parallax correction OBB matrix" )
	SHADER_PARAM( ENVMAPPARALLAXOBB3, SHADER_PARAM_TYPE_VEC4, "[0 0 1 0]", "The third line of the parallax correction OBB matrix" )
	SHADER_PARAM( ENVMAPORIGIN, SHADER_PARAM_TYPE_VEC3, "[0 0 0]", "The world space position of the env_cubemap being corrected" )

In the same file, inside the

void SetupVars( LightmappedGeneric_DX9_Vars_t& info )

function, add the following lines at the end (after info.m_nOutlineEnd1 = OUTLINEEND1;):

	// Parallax cubemaps
	info.m_nEnvmapParallaxObb1 = ENVMAPPARALLAXOBB1;
	info.m_nEnvmapParallaxObb2 = ENVMAPPARALLAXOBB2;
	info.m_nEnvmapParallaxObb3 = ENVMAPPARALLAXOBB3;
	info.m_nEnvmapOrigin = ENVMAPORIGIN;

lightmappedgeneric_dx9_helper.cpp

Inside

void InitParamsLightmappedGeneric_DX9( CBaseVSShader *pShader, IMaterialVar** params, const char *pMaterialName, LightmappedGeneric_DX9_Vars_t &info )

add the following lines after InitFloatParam( info.m_nOutlineAlpha, params, 1.0 );:

	// Parallax cubemaps
	// Cubemap parallax correction requires all 4 lines (if the 2nd, 3rd, or 4th are undef, undef the first one (checking done on first var)
	if ( !( params[info.m_nEnvmapParallaxObb2]->IsDefined() && params[info.m_nEnvmapParallaxObb3]->IsDefined() && params[info.m_nEnvmapOrigin]->IsDefined() ) )
	{
		params[info.m_nEnvmapParallaxObb1]->SetIntValue( 0 );
	}

In the

void DrawLightmappedGeneric_DX9_Internal( ... )

function, after

	bool hasNormalMapAlphaEnvmapMask = IS_FLAG_SET( MATERIAL_VAR_NORMALMAPALPHAENVMAPMASK );

add:

	// Parallax cubemaps
	bool hasParallaxCorrection = params[info.m_nEnvmapParallaxObb1]->GetType() == MATERIAL_VAR_TYPE_VECTOR;

Within the if ( g_pHardwareConfig->SupportsPixelShaders_2_b() ) block, add before

	SET_STATIC_PIXEL_SHADER( lightmappedgeneric_ps20b );

the following:

	// Parallax cubemaps enabled for 2_0b and onwards
	SET_STATIC_PIXEL_SHADER_COMBO( PARALLAXCORRECT, hasParallaxCorrection );

In the else statement for the above, before SET_STATIC_PIXEL_SHADER( lightmappedgeneric_ps20 );, add:

	// Parallax cubemaps
	SET_STATIC_PIXEL_SHADER_COMBO( PARALLAXCORRECT, 0 ); // No parallax cubemaps with ps_2_0 :(

Before pContextData->m_SemiStaticCmdsOut.End();, add:

	// Parallax cubemaps
	if ( hasParallaxCorrection )
	{
		pContextData->m_SemiStaticCmdsOut.SetPixelShaderConstant( 21, params[info.m_nEnvmapOrigin]->GetVecValue() );

		float *vecs[3];
		vecs[0] = const_cast<float *>( params[info.m_nEnvmapParallaxObb1]->GetVecValue() );
		vecs[1] = const_cast<float *>( params[info.m_nEnvmapParallaxObb2]->GetVecValue() );
		vecs[2] = const_cast<float *>( params[info.m_nEnvmapParallaxObb3]->GetVecValue() );
		float matrix[4][4];
		for ( int i = 0; i < 3; i++ )
		{
			for ( int j = 0; j < 4; j++ )
			{
				matrix[i][j] = vecs[i][j];
			}
		}
		matrix[3][0] = matrix[3][1] = matrix[3][2] = 0;
		matrix[3][3] = 1;
		pContextData->m_SemiStaticCmdsOut.SetPixelShaderConstant( 22, &matrix[0][0], 4 );
	}

The Shader Files

lightmappedgeneric_ps2_3_x.h

Below

const float4 g_ShadowTweaks : register( c19 );

add:

// Parallax cubemaps
#if ( PARALLAXCORRECT )
const float3 g_CubemapPos : register( c21 );
const float4x4 g_ObbMatrix : register( c22 ); // Through c25
#endif

Within the cubemap processing block, after fresnel = fresnel * g_OneMinusFresnelReflection + g_FresnelReflection;, add:

	// Parallax correction (2_0b and beyond)
	// Adapted from http://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/
#if !( defined( SHADER_MODEL_PS_1_1 ) || defined( SHADER_MODEL_PS_1_4 ) || defined( SHADER_MODEL_PS_2_0 ) )
#if ( PARALLAXCORRECT )
	float3 worldPos = i.worldPos_projPosZ.xyz;
	float3 positionLS = mul( float4( worldPos, 1 ), g_ObbMatrix );
	float3 rayLS = mul( reflectVect, (float3x3)g_ObbMatrix );

	float3 firstPlaneIntersect = ( float3( 1.0f, 1.0f, 1.0f ) - positionLS ) / rayLS;
	float3 secondPlaneIntersect = ( -positionLS ) / rayLS;
	float3 furthestPlane = max( firstPlaneIntersect, secondPlaneIntersect );
	float distance = min( furthestPlane.x, min( furthestPlane.y, furthestPlane.z ) );

	// Use distance in WS directly to recover intersection
	float3 intersectPositionWS = worldPos + reflectVect * distance;
	reflectVect = intersectPositionWS - g_CubemapPos;
#endif
#endif

lightmappedgeneric_ps2x.fxc

After // STATIC: "FLASHLIGHT" "0..1" [ps20b] [XBOX], add:

// STATIC: "PARALLAXCORRECT" "0..1"

After // SKIP ($DETAIL_BLEND_MODE == 11 ) && ($BUMPMAP != 0 ), add:

// SKIP: $PARALLAXCORRECT && !$CUBEMAP
// SKIP: $PARALLAXCORRECT [ps20]

Compile the Shader(s)

Ensure the following shaders are listed in 🖿stdshader_dx9_20b.txt (or the custom game file):

lightmappedgeneric_ps2x.fxc
lightmappedgeneric_ps11.fxc
lightmappedgeneric_vs20.fxc

Compile the shaders. This process may take a significant amount of time. Upon completion, the appropriate Filelightmappedgeneric_*.inc and Filelightmappedgeneric_*.vcs files will be generated. Copy these to the game's 🖿shaders/fxc/ folder.

With the new .inc files, compile the Filegame_shader_dx9.sln project. The resulting Filegame_shader_dx9.dll should be copied to the game's 🖿bin/ folder, alongside the server and client DLLs.

What Next?

The FGD File

parallaxcubes.fgd
@include "YOUR FGD HERE. THIS ONE WILL OVERRIDE THE REGULAR ENV_CUBEMAP ENTITY AND ALSO ADD THE PARALLAX_OBB ENTITY. ONLY INCLUDE THIS ONE IN HAMMER." @PointClass color(0 0 255) sidelist(sides) iconsprite("editor/env_cubemap.vmt") = env_cubemap : "An entity that creates a sample point for the Cubic Environment Map." [ cubemapsize(choices) : "Cubemap Size" : 0 = [ 0 : "Default" 1 : "1x1" 2 : "2x2" 3 : "4x4" 4 : "8x8" 5 : "16x16" 6 : "32x32" 7 : "64x64" 8 : "128x128" 9 : "256x256" ] sides(sidelist) : "Brush faces": : "(Optional) Brushes faces to directly attach to the env_cubemap. Press Pick then click on faces in the 3D View to select them. Use CTRL while clicking to add or remove from the selection." parallaxobb(target_destination) : "Cubemap Bounds" : : "(Optional) assigns this cubemap a bounding box for parallax correction (brush entity tied to parallax_obb)." ] @SolidClass = parallax_obb [ targetname(target_source) : "Name" : : "The name that other entities refer to this entity by." ]

(Optional) Test Map

Instructions for creating Parallax Corrected Cubemaps can be found in the Parallax Corrected Cubemaps/Creation article.

Todo: Test Map coming soon

Conclusion

At this point, the mod should be ready to launch with parallax-corrected cubemaps enabled.