Replacement shaders
This page is for modders who want to change how the game looks by writing their own shading programs. You don’t need it to play.
You can put your own shader files in the game folder, and the patch uses them in place of the programs it generates. You don’t need to rebuild anything or use any tools. Each file is plain text in ARB fragment program assembly, the same language the patch’s own programs use.
Where replacement files go
Put replacement files in override/shaders/ in the game folder. If the folder doesn’t exist,
create it.
The folder is inside override so that a mod can install replacement shaders with
TSLPatcher, like any other override file. The files are in a subfolder because they aren’t game
resources. The game never reads them: it searches override for the file types it knows, it
doesn’t search subfolders, and .arb isn’t one of its file types. Only the patch reads them.
How to name a replacement file
Each file replaces one program and is named after it:
- World shaders, which draw surfaces, take a fog variant:
<name>_<fog>.arb, where<fog>isoff,linear,exp, orexp2. - Effect shaders, which draw screen effects, are named
<name>.arb. Fog never reaches screen effects, so they have no fog variants. brightness.arbapplies the brightness slider to the finished frame. It reads the frame on texture unit 0 and the slider’s curve on unit 1, with one texel for each byte value.downsample.arbshrinks the images for the glow and soft shadows. It reads the larger image on texture unit 0, and the values thatDownsample::localsdescribes fromprogram.local[0]toprogram.local[3].
The patch ignores capitalisation in file names, so a file named on Linux loads on Windows, and the reverse.
The program names are:
flat diffuseBlend lightmap envMap2D
envMapCube envThreeTex2D envThreeTexCube envMapBlend2D
envMapBlendCube normalTransform bumpShinyA bumpShinyB
lightmapBumpDiffuse lightmapBumpSpecular1
overbrighten forceDistortion filmNoise accumulationBlur
saturation perPixelFlare decal decal4
softShadow
brightness downsample
These are the names the log uses. To find the program a surface uses, set log = debug. The log
then records each world program and fog variant the first time the patch binds it, and each
effect program the first time the game uses it. For example, a line that contains
probe mode=1A shader=lightmap fog=Linear means that the file to write is lightmap_linear.arb,
and a line that contains probe effect=filmNoise means filmNoise.arb.
What a replacement file contains
Each file is a complete program, from !!ARBfp1.0 to END. The patch adds nothing to it. In
particular, each world shader variant applies fog itself, which is why there are four variants.
If you replace a fogged variant, your program must apply the fog, or that surface stops fogging.
Three world shaders are an exception: diffuseBlend, envMapBlend2D, and envMapBlendCube
already fade towards the fog colour in their own code, as the game’s programs do. All four
variants of each are the same program, without the fog step in the following example. The game
turns the fixed function fog off while it draws these three, so only the _off variant of each
ever runs. If you replace one of those three, write <name>_off.arb and leave fog alone, or the
surface fades to the fog colour twice.
This is the generated lightmap_linear.arb for the Radeon look. The last five instructions apply
the fog:
!!ARBfp1.0
PARAM fogParam = state.fog.params;
PARAM fogColour = state.fog.color;
TEMP fogCoord, fogFactor;
TEMP oCol;
TEMP base, lmap, sum;
TEX base, fragment.texcoord[0], texture[0], 2D;
TEX lmap, fragment.texcoord[1], texture[1], 2D;
ADD sum, fragment.color.primary, lmap;
MUL oCol.rgb, base, sum;
MUL oCol.a, fragment.color.primary, base;
ABS fogCoord.x, fragment.fogcoord.x;
SUB fogFactor.x, fogParam.z, fogCoord.x;
MUL_SAT fogFactor.x, fogFactor.x, fogParam.w;
LRP result.color.rgb, fogFactor.x, oCol, fogColour;
MOV result.color.a, oCol;
END
In the GeForce look, the default, ADD is ADD_SAT. That is the only difference between the
two looks in this program.
The text of every world and effect program for the Radeon look is in the repository, in
crates/arbfp-shaders/tests/golden/.
A replacement is easiest to start from the program it replaces.
Programs that differ between the two looks
Nine programs differ between the two looks:
lightmap,envThreeTex2D,envThreeTexCube,envMap2D,envMapCube,envMapBlend2D, andenvMapBlendCubelimit a step that can pass full brightness. This is the brightness difference that Choose between the two looks describes.bumpShinyAandbumpShinyBlimit two dot products to zero or above, as the register combiners do, before the highlight is squared. Without that limit, a negative value becomes positive when squared, and lights the side of a surface that faces away from the light. The Hutts in the Taris cantinas show this difference, most on the parts that face away from the light. The GeForce versions also drop the highlight where the half vector points into the surface.
Every other program is the same in both looks. This includes lightmapBumpDiffuse and
lightmapBumpSpecular1: the game has no ATI version of those modes, so both looks use the
translation of the register combiner setup.
A replacement file replaces its program in both looks, because file names don’t include the look. To keep one look’s behaviour for one of the nine programs, write the replacement from that look’s version.
What your program receives
A replacement changes only the program text. The patch binds the same textures and sets the same constants as for its own program.
Your program receives two kinds of input:
- Textures, on numbered texture units. Read them with
TEX dst, fragment.texcoord[n], texture[n], 2D, orCUBEor1Dfor those targets. - Values the game computes for each draw, as
program.env[n]. Your program can read these but not write them.
World programs:
| Program | Texture units | program.env[n] |
|---|---|---|
flat | None | None |
diffuseBlend | 0 diffuse | [1] colour to blend towards |
lightmap | 0 diffuse, 1 lightmap | None |
envMap2D | 0 diffuse, 1 environment | [0].a output alpha |
envMapCube | 0 diffuse, 1 environment (cube) | [0].a output alpha |
envMapBlend2D | 0 diffuse, 1 environment | [0].a output alpha, [1] blend colour |
envMapBlendCube | 0 diffuse, 1 environment (cube) | [0].a output alpha, [1] blend colour |
envThreeTex2D | 0 diffuse, 1 lightmap, 2 environment | [0].a output alpha |
envThreeTexCube | 0 diffuse, 1 lightmap, 2 environment (cube) | [0].a output alpha |
normalTransform | 0 normal map, 1 to 3 basis (cube), 4 refinement (cube), 5 environment (cube) | [0].a output alpha |
bumpShinyA | 0 normal map, 1 to 2 basis (cube), 3 modulating | [0], [1] light colours |
bumpShinyB | 0 modulating, 1 normal map, 2 to 3 basis (cube) | [0], [1] light colours |
lightmapBumpDiffuse | 0 normal map, 1 light vector (cube), 2 lightmap, 3 diffuse | [1] light colour |
lightmapBumpSpecular1 | 0 normal map, 1 half vector (cube), 2 lightmap, 3 diffuse | [0] light colour |
Effect programs, each of which shades a rectangle that covers the screen:
| Program | Texture units | program.env[n] | Output alpha |
|---|---|---|---|
overbrighten | 0 frame, 1 to 3 tone curve tables (1D) | [0] to [2] colour matrix rows | Primary colour alpha |
perPixelFlare | 0 frame, 1 to 3 tone curve tables (1D) | [0] to [2] colour matrix rows | 1 |
forceDistortion | 0 normal map, 2 frame | None | Normal map alpha |
filmNoise | 0 frame, 1 noise | None | 1 |
accumulationBlur | 0 frame, 1 accumulation buffer | [0], [1] weights | [0].a |
saturation | 0 frame | [0] to [2] luminance rows, [3] to [5] weights for each channel | Frame alpha |
decal | 0 frame | None | 1 |
decal4 | 0 to 3, four samples of the frame | [0] to [3] sample weights | Primary colour alpha |
softShadow | 0 to 3, four samples of the frame | [0] to [3] sample weights | The weighted sum’s alpha |
Inputs to check
- Name the texture target that is bound. Each
TEXinstruction names the target it samples, and the wrong target isn’t a compile error. The tone curve tables on units 1 to 3 ofoverbrightenandperPixelFlareare1D, not2D. If you name2D, the program samples an incomplete target, and several drivers return white. The same applies to everyCUBEin the preceding tables, which is why the environment mapped programs come in2DandCubepairs. The brightness curve on unit 1 ofbrightnessis1Das well. - In the reflective programs, the alpha of the diffuse texture is the reflectivity mask. An alpha of 1 keeps the lit texel, and 0 shows the reflection.
forceDistortionreads the frame on unit 2, not unit 0. Unit 0 is the normal map, andfragment.texcoord[1]andfragment.texcoord[2]are the basis vectors it takes dot products with.- Write the alpha the original writes. The game blends many draws by the alpha a program
writes, so a replacement that writes a different alpha changes how its surface or effect
blends, and can stop a surface fading. The effect table gives the alpha each effect writes,
and the world table marks the programs that take theirs from
program.env[0].a. For the rest, read the alpha from the patch’s own version of the program. - The interpolated colours are inputs too.
fragment.color.primaryis the lit vertex colour, andfragment.color.secondary.ris the amount the blend modes fade by. In the two lightmapped bump programs, the secondary colour is the light’s attenuation.
Check that your replacement loaded
When the patch loads a replacement, it writes a line like this to the log:
[ 0.412] [INFO] program override envMapCube_linear.arb
If a file’s name doesn’t match any program, the patch writes a warning instead. A misnamed replacement looks the same as no replacement in the game, so this warning is how you find one:
[ 0.412] [WARN] program override envMapCube.arb matches no program and was ignored
If the driver refuses to compile a replacement, the log reports the position where compiling stopped and the driver’s message. The patch doesn’t fall back to its own program. What happens instead depends on the program:
- A refused world program leaves the surfaces that use it drawn without shading.
- A refused effect program leaves that effect without a program.
- A refused
brightnessprogram leaves the frame without the brightness pass. - A refused
downsampleprogram leaves the glow and soft shadows to the game’s own shrink.
Known limitations
overbrightenis never used. The game uses it only when a flag at0x007A68BCis set, and nothing in the game sets that flag. A replacement foroverbrightenhas no effect.bumpShinyBdraws only normal mapped surfaces, whose bump map is anisbumpmap 2texture at 32 bits per texel, on meshes without tangents. The game reaches it throughMaterial::HasNormalMap, which every build of the game compiles to return false. The patch answers that function, so the program runs for such a mesh. No mesh in the shipped game has a normal map, so a replacement forbumpShinyBshows only on mod content.lightmapBumpDiffuseandlightmapBumpSpecular1draw only lightmapped bump mapped surfaces. In the shipped game, only a test model that no level loads has one, so a replacement for either shows only on mod content.- Only the
offandlinearfog variants have been seen in play. The patch still builds and tests theexpandexp2variants.
Programs from other games
These programs aren’t the game’s own shaders. They’re translations of the game’s
ATI_fragment_shader constructions and register combiner setups, so they expect this game’s
texture units and constants, as the preceding tables list them. A program written for another
game, including KOTOR 2, needs remapping to those inputs before it renders correctly here.