Material proxies: Difference between revisions
m (categorise) |
m (Setting bug notice hidetested=1 param on page where the bug might not need tested in param specified) |
||
(50 intermediate revisions by 28 users not shown) | |||
Line 1: | Line 1: | ||
{{LanguageBar}} | |||
{{toc-right}} | |||
Material proxies allow | '''Material proxies''' allow a game's compiled C++ code to manipulate the properties of a [[material]] in-game. They can be used to create dynamic or animated textures. | ||
< | == Usage == | ||
Material proxies can be added to some material via its [[VMT]] file, more specificly in a [[KeyValues]]-block named <code>Proxies</code>; Each entry inside it is what we name a '''proxy'''. Any number of proxies can be added to a material. Proxies are executed in the order in which they appear. The game repeatedly executes the list of a material's proxies in very quick succession.{{inline note|name=every tick?}} | |||
The name of the proxy determines what parameters it has and what the proxy does. | |||
Many proxies perform specific tasks, but there are other more general ones that together provide rudimentary scripting support within VMT files.{{clarify}} | |||
{{main|List of material proxies}} | |||
Typically, each proxy has an output value (e. g. a [[float]] number) which will be written to its <code>resultVar</code> which can be either a shader parameter or variable (see [[#Variables|below]] for variables). | |||
For example, the following material has a {{material proxy|Sine}} proxy which generates a float number and writes it to the material's {{ent|$alpha}} shader parameter, which makes the texture fade in and out of view over a period of eight seconds: | |||
<source lang=php> | |||
LightmappedGeneric | |||
{ | { | ||
$basetexture shadertest/LightmappedTexture | |||
// | Proxies // proxies are listed inside this block | ||
{ | { | ||
// | Sine // a proxy which produces values of a sine wave | ||
{ | { | ||
// | sineperiod 8 // the sine cycle length in seconds | ||
// | sinemin 0 // min output value | ||
sinemax 1 // max output value | |||
resultVar $alpha // where the oscillating output value is written to | |||
} | } | ||
} | } | ||
} | } | ||
</ | </source> | ||
=== Variables === | |||
Materials can declare their own variables for internal use. Such variables must be declared outside the <code>Proxies</code> block, in the body of the material, and must have a default value specified on the right-hand side. | |||
These custom variables can be used to pass results between proxies or to submit hard-coded data to them. They are often employed to chain mathematic function proxies (i. e. {{material proxy|Add}}, {{material proxy|Subtract}}, etc.) together into longer equations. For 2D/3D/4D vectors (<code>$vec "[0 0 0]"</code>), the variable name can have <code>[0]</code> suffixed (<code>"$vec[0]"</code>) to read/write a specific index. Writes to indexed variables should be encased in quotes. | |||
< | The engine encodes these custom variables using a 8-bit signed integer. Therefore, there is a limit of 128 unique custom variables per material. | ||
{ | This example extends the one above by staggering the starting position of the sine wave with a random value produced with the {{material proxy|EntityRandom}} proxy: | ||
// | <source lang=php> | ||
LightmappedGeneric | |||
{ | |||
$basetexture shadertest/LightmappedTexture | |||
$offset 0 // declare custom var ($offset is not a shader parameter the game knows) | |||
Proxies | |||
{ | { | ||
EntityRandom // generates a random number | |||
{ | |||
resultVar $offset // write to custom var | |||
} | |||
Sine | |||
{ | |||
resultVar $alpha | |||
timeoffset $offset // read from custom var | |||
sineperiod 8 | |||
sinemin 0 | |||
sinemax 1 | |||
} | |||
} | } | ||
} | |||
" | </source> | ||
Now each entity this material is used on pulses to its own schedule. | |||
This is an example for writing to indexed variables using the $color vector to create 'random' color pulses: | |||
<source lang=php> | |||
$color "[0 0 0]" // not custom, the game knows "$color" | |||
proxies | |||
{ | { | ||
. . . | sine | ||
{ | |||
sineperiod 1.3 | |||
sinemin 0 | |||
sinemax 1 | |||
timeoffset 0 | |||
resultvar "$color[0]" | |||
} | |||
sine | |||
{ | |||
sineperiod 1.7 | |||
sinemin 0 | |||
sinemax 1 | |||
timeoffset 0 | |||
resultvar "$color[1]" | |||
} | |||
sine | |||
{ | |||
sineperiod 2.3 | |||
sinemin 0 | |||
sinemax 1 | |||
timeoffset 0 | |||
resultvar "$color[2]" | |||
} | |||
} | } | ||
</source> | |||
</ | |||
< | {{expand|noborder=1|title=Example of a dynamic texture transform| | ||
<source lang=php> | |||
UnlitGeneric | |||
{ | { | ||
"$ | $basetexture "dev\gradient_dif" | ||
$color "[1 .8 .6]" | |||
$detail "dev\noise_512x32" | |||
$detailscale 1 | |||
$detailblendmode 0 | |||
$detailblendfactor 4.0 | |||
$additive 1 | |||
$nocull 1 | |||
$cvar "[.5 .5]" | |||
$svar "[1 .25]" | |||
$rvar 0 | |||
$tvar "[0 0]" | |||
$sine1 0 | |||
$sine2 0 | |||
proxies | |||
{ | { | ||
. . . | linearramp | ||
{ | |||
rate .3 | |||
initialvalue 0 | |||
resultvar "$tvar[1]" | |||
} | |||
sine | |||
{ | |||
sineperiod 1.3 | |||
sinemin -.004 | |||
sinemax .002 | |||
timeoffset 0 | |||
resultvar $sine1 | |||
} | |||
sine | |||
{ | |||
sineperiod 1.7 | |||
sinemin -.003 | |||
sinemax .007 | |||
timeoffset .2 | |||
resultvar $sine2 | |||
} | |||
add | |||
{ | |||
srcvar1 $sine1 | |||
srcvar2 $sine2 | |||
resultvar "$tvar[0]" | |||
} | |||
texturetransform | |||
{ | |||
centervar $cvar | |||
scalevar $svar | |||
rotatevar $rvar | |||
translatevar $tvar | |||
resultvar $detailtexturetransform | |||
} | |||
} | } | ||
} | } | ||
</ | </source> | ||
}} | |||
=== Splitting a vector === | |||
Using vectors is quirky, because vector component expressions such as <code>"$pos[0]"</code> are not always recognized by the game. | |||
{{important|Quotes are needed when addressing a vector's component, or when defining a vector.}} | |||
The following proxies, and only these keyvalues, can recognize vector components: | |||
<tt> | |||
* The <code>resultVar</code> of all proxies | |||
* {{material proxy|Clamp}}: min, max | |||
* {{material proxy|Sine}}: offset, max, min, period | |||
* {{material proxy|LinearRamp}}: rate, initial value | |||
* {{material proxy|UniformNoise}}: min, max | |||
* {{material proxy|GaussianNoise}}: min, max, mean, halfwidth | |||
* {{material proxy|WrapMinMax}}: min, max | |||
* {{material proxy|Exponential}}: min, max, scale, offset | |||
</tt> | |||
If a vector's components need be processed separately, they need to be split into different variables first. | |||
All of the above can be used to split a vector. However, Clamp is the [[Cheap|cheapest]] to use. | |||
{{expand|noborder=1|title=Example| | |||
<source lang="php"> | |||
$pos "[0 0 0]" | |||
$posX .0 //must be float or Clamp will not save the value properly | |||
$posY .0 //must be float or Clamp will not save the value properly | |||
$posZ .0 //must be float or Clamp will not save the value properly | |||
$zero 0 | |||
//Proxy that outputs a 3d vector | |||
PlayerPosition | |||
{ | |||
scale 1 | |||
resultVar "$pos" | |||
} | |||
//Split the 3d vector for further use | |||
Clamp | |||
{ | |||
srcVar1 $zero | |||
min "$pos[0]" | |||
max "$pos[0]" | |||
resultVar $posX | |||
} | |||
Clamp | |||
{ | |||
srcVar1 $zero | |||
min "$pos[1]" | |||
max "$pos[1]" | |||
resultVar $posY | |||
} | |||
Clamp | |||
{ | |||
srcVar1 $zero | |||
min "$pos[2]" | |||
max "$pos[2]" | |||
resultVar $posZ | |||
} | |||
</source> | |||
}} | |||
== Writing new proxies == | |||
New proxies are easy to create. They exist on the client only and should inherit from <code>IMaterialProxy</code> or one of its descendants. | |||
You will need these #includes: | |||
* <code>"materialsystem/IMaterialProxy.h"</code> | |||
* <code>"materialsystem/IMaterialVar.h"</code> | |||
These functions are included in the interface: | |||
< | ; <code>[[bool]] Init( [[IMaterial]]* pMaterial, [[KeyValues]]* pKeyValues )</code> | ||
bool Init( IMaterial *pMaterial, KeyValues *pKeyValues ); | : Called when the material is first [[precache]]d. Use this function to initialise variables and grab references to the material vars you will be using. Return true on success and false on failure (in which case the proxy will not be run). | ||
: <code>pKeyValues</code> contains the proxy parameters from the VMT file. | |||
; <code>void OnBind( void* pC_BaseEntity )</code> | |||
: Called when the material is about to be rendered on an entity. This is where the work is done. | |||
: When coding this function it is important to remember that all entities using a material share the same material object, and that if you change it on one entity it changes everywhere else too. Since <code>OnBind()</code> is called every time an entity comes up for rendering this is not a problem ''so long as you reassign the value you want every time''. Don't return early just because there has been no change, and don't store any input data in the proxy. {{note|<code>pC_BaseEntity</code> doesn't lead to a <code>[[C_BaseEntity]]</code> as its name suggests, but rather to the associated <code>[[IClientRenderable]]</code>. The easiest way to access the entity directly is to base your class on <code>CEntityMaterialProxy</code> (in <code>proxyentity.h</code>) and use the <code>OnBind(C_BaseEntity*)</code> overload it provides.}} | |||
; <code>void Release()</code> | |||
: {{todo|Called when the proxy is removed, but when is that?}} | |||
; <code>[[IMaterial]]* GetMaterial()</code> | |||
: The material the proxy is attached to. {{tip|If you have a material var stored, you can return <code>IMaterialVar::GetOwningMaterial()</code> here instead of creating a new <code>IMaterial</code> pointer.}} | |||
=== Interface === | |||
The <code> | The proxy must expose its interface to materials with the <code>EXPOSE_INTERFACE</code> macro: | ||
< | <source lang=cpp> | ||
EXPOSE_INTERFACE( <className>, <interfaceName>, "<proxyName>" IMATERIAL_PROXY_INTERFACE_VERSION ); | |||
</source> | |||
</ | |||
The | The lack of a comma between the proxy name and interface version is intentional. | ||
=== Tools recording === | |||
This code was added to all proxies in the Orange Box: | |||
<source lang=cpp> | |||
#include "toolframework_client.h" | |||
void OnBind(...) | |||
{ | |||
//... | |||
if ( ToolsEnabled() ) | |||
ToolFramework_RecordMaterialParams( GetMaterial() ); | |||
} | |||
</source> | |||
'' | It's probably related to the [[Source Filmmaker]]. It's a good idea to add it to your proxy too in case the Filmmaker is ever released! | ||
{{tip|<code>CEntityMaterialProxy</code> makes the call by itself.}} | |||
| <code> | == Bugs == | ||
{{bug|hidetested=1|The first parameter in a material cannot be written to by proxies. A simple workaround is to add a dummy variable at the top of the VMT.}} | |||
{{bug|hidetested=1|Some users have reported that the tools mode will not run some functional proxies in game. Extent of bug untested.}} | |||
{{bug|hidetested=1|Using material proxies that access entity state, such as <code>EntityRandom</code> on particles or [[prop_static]], may cause the game to crash.}} | |||
| | |||
| <code> | |||
== See also == | == See also == | ||
* [[List Of Material Proxies]] | |||
* [[Material proxies programming]] | |||
* [[Material Creation]] | * [[Material Creation]] | ||
* Steam guides, explaining more about various Proxies. [https://steamcommunity.com/sharedfiles/filedetails/?id=594255575 Basics], [https://steamcommunity.com/sharedfiles/filedetails/?id=668958242 RNG], [https://steamcommunity.com/sharedfiles/filedetails/?id=749130424 "Programming"] | |||
== External links == | |||
* [https://nodraw.net/2010/01/dynamic-materials-with-proxies/ Dynamic Materials With Proxies] - An article covering practical uses of materials with proxies by [http://www.nodraw.net NoDraw.net] | |||
[[Category:Source]] | |||
[[Category:Material System]] | [[Category:Material System]] | ||
[[Category:C++]] |
Latest revision as of 07:07, 20 May 2025
Material proxies allow a game's compiled C++ code to manipulate the properties of a material in-game. They can be used to create dynamic or animated textures.
Usage
Material proxies can be added to some material via its VMT file, more specificly in a KeyValues-block named Proxies
; Each entry inside it is what we name a proxy. Any number of proxies can be added to a material. Proxies are executed in the order in which they appear. The game repeatedly executes the list of a material's proxies in very quick succession.[every tick?]
The name of the proxy determines what parameters it has and what the proxy does. Many proxies perform specific tasks, but there are other more general ones that together provide rudimentary scripting support within VMT files.[Clarify]
Typically, each proxy has an output value (e. g. a float number) which will be written to its resultVar
which can be either a shader parameter or variable (see below for variables).
For example, the following material has a Sine
proxy which generates a float number and writes it to the material's $alpha shader parameter, which makes the texture fade in and out of view over a period of eight seconds:
LightmappedGeneric
{
$basetexture shadertest/LightmappedTexture
Proxies // proxies are listed inside this block
{
Sine // a proxy which produces values of a sine wave
{
sineperiod 8 // the sine cycle length in seconds
sinemin 0 // min output value
sinemax 1 // max output value
resultVar $alpha // where the oscillating output value is written to
}
}
}
Variables
Materials can declare their own variables for internal use. Such variables must be declared outside the Proxies
block, in the body of the material, and must have a default value specified on the right-hand side.
These custom variables can be used to pass results between proxies or to submit hard-coded data to them. They are often employed to chain mathematic function proxies (i. e. Add
, Subtract
, etc.) together into longer equations. For 2D/3D/4D vectors ($vec "[0 0 0]"
), the variable name can have [0]
suffixed ("$vec[0]"
) to read/write a specific index. Writes to indexed variables should be encased in quotes.
The engine encodes these custom variables using a 8-bit signed integer. Therefore, there is a limit of 128 unique custom variables per material.
This example extends the one above by staggering the starting position of the sine wave with a random value produced with the EntityRandom
proxy:
LightmappedGeneric
{
$basetexture shadertest/LightmappedTexture
$offset 0 // declare custom var ($offset is not a shader parameter the game knows)
Proxies
{
EntityRandom // generates a random number
{
resultVar $offset // write to custom var
}
Sine
{
resultVar $alpha
timeoffset $offset // read from custom var
sineperiod 8
sinemin 0
sinemax 1
}
}
}
Now each entity this material is used on pulses to its own schedule.
This is an example for writing to indexed variables using the $color vector to create 'random' color pulses:
$color "[0 0 0]" // not custom, the game knows "$color"
proxies
{
sine
{
sineperiod 1.3
sinemin 0
sinemax 1
timeoffset 0
resultvar "$color[0]"
}
sine
{
sineperiod 1.7
sinemin 0
sinemax 1
timeoffset 0
resultvar "$color[1]"
}
sine
{
sineperiod 2.3
sinemin 0
sinemax 1
timeoffset 0
resultvar "$color[2]"
}
}
UnlitGeneric
{
$basetexture "dev\gradient_dif"
$color "[1 .8 .6]"
$detail "dev\noise_512x32"
$detailscale 1
$detailblendmode 0
$detailblendfactor 4.0
$additive 1
$nocull 1
$cvar "[.5 .5]"
$svar "[1 .25]"
$rvar 0
$tvar "[0 0]"
$sine1 0
$sine2 0
proxies
{
linearramp
{
rate .3
initialvalue 0
resultvar "$tvar[1]"
}
sine
{
sineperiod 1.3
sinemin -.004
sinemax .002
timeoffset 0
resultvar $sine1
}
sine
{
sineperiod 1.7
sinemin -.003
sinemax .007
timeoffset .2
resultvar $sine2
}
add
{
srcvar1 $sine1
srcvar2 $sine2
resultvar "$tvar[0]"
}
texturetransform
{
centervar $cvar
scalevar $svar
rotatevar $rvar
translatevar $tvar
resultvar $detailtexturetransform
}
}
}
|
Splitting a vector
Using vectors is quirky, because vector component expressions such as "$pos[0]"
are not always recognized by the game.

The following proxies, and only these keyvalues, can recognize vector components:
- The
resultVar
of all proxies Clamp
: min, maxSine
: offset, max, min, periodLinearRamp
: rate, initial valueUniformNoise
: min, maxGaussianNoise
: min, max, mean, halfwidthWrapMinMax
: min, maxExponential
: min, max, scale, offset
If a vector's components need be processed separately, they need to be split into different variables first. All of the above can be used to split a vector. However, Clamp is the cheapest to use.
$pos "[0 0 0]"
$posX .0 //must be float or Clamp will not save the value properly
$posY .0 //must be float or Clamp will not save the value properly
$posZ .0 //must be float or Clamp will not save the value properly
$zero 0
//Proxy that outputs a 3d vector
PlayerPosition
{
scale 1
resultVar "$pos"
}
//Split the 3d vector for further use
Clamp
{
srcVar1 $zero
min "$pos[0]"
max "$pos[0]"
resultVar $posX
}
Clamp
{
srcVar1 $zero
min "$pos[1]"
max "$pos[1]"
resultVar $posY
}
Clamp
{
srcVar1 $zero
min "$pos[2]"
max "$pos[2]"
resultVar $posZ
}
|
Writing new proxies
New proxies are easy to create. They exist on the client only and should inherit from IMaterialProxy
or one of its descendants.
You will need these #includes:
"materialsystem/IMaterialProxy.h"
"materialsystem/IMaterialVar.h"
These functions are included in the interface:
bool Init( IMaterial* pMaterial, KeyValues* pKeyValues )
- Called when the material is first precached. Use this function to initialise variables and grab references to the material vars you will be using. Return true on success and false on failure (in which case the proxy will not be run).
pKeyValues
contains the proxy parameters from the VMT file.void OnBind( void* pC_BaseEntity )
- Called when the material is about to be rendered on an entity. This is where the work is done.
- When coding this function it is important to remember that all entities using a material share the same material object, and that if you change it on one entity it changes everywhere else too. Since
OnBind()
is called every time an entity comes up for rendering this is not a problem so long as you reassign the value you want every time. Don't return early just because there has been no change, and don't store any input data in the proxy.Note:
pC_BaseEntity
doesn't lead to aC_BaseEntity
as its name suggests, but rather to the associatedIClientRenderable
. The easiest way to access the entity directly is to base your class onCEntityMaterialProxy
(inproxyentity.h
) and use theOnBind(C_BaseEntity*)
overload it provides. void Release()
- Todo: Called when the proxy is removed, but when is that?
IMaterial* GetMaterial()
- The material the proxy is attached to.
Tip:If you have a material var stored, you can return
IMaterialVar::GetOwningMaterial()
here instead of creating a newIMaterial
pointer.
Interface
The proxy must expose its interface to materials with the EXPOSE_INTERFACE
macro:
EXPOSE_INTERFACE( <className>, <interfaceName>, "<proxyName>" IMATERIAL_PROXY_INTERFACE_VERSION );
The lack of a comma between the proxy name and interface version is intentional.
Tools recording
This code was added to all proxies in the Orange Box:
#include "toolframework_client.h"
void OnBind(...)
{
//...
if ( ToolsEnabled() )
ToolFramework_RecordMaterialParams( GetMaterial() );
}
It's probably related to the Source Filmmaker. It's a good idea to add it to your proxy too in case the Filmmaker is ever released!

CEntityMaterialProxy
makes the call by itself.Bugs



EntityRandom
on particles or prop_static, may cause the game to crash.See also
- List Of Material Proxies
- Material proxies programming
- Material Creation
- Steam guides, explaining more about various Proxies. Basics, RNG, "Programming"
External links
- Dynamic Materials With Proxies - An article covering practical uses of materials with proxies by NoDraw.net